Canonical secrets policy for agents + best-effort redaction
deploy / deploy (push) Canceled after 0s
deploy / deploy (push) Canceled after 0s
- /security.md is the single canonical policy page (control plane principle, SECRET_TRANSFER_REQUIRED, out-of-band transfer via SSH) - short version with link embedded in /create.md, room .md, llms.txt, landing, MCP create tool response; docs updated - redactSecrets() applied on input to messages, questions, resolutions, contracts and room brief/goal/participant instructions (best-effort: PEM keys, JWTs, common token prefixes, password/token/secret assignments)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import { RendezvousService, Store, renderFinalMarkdown, renderRoomMarkdown, renderObserverMarkdown, RendezvousError, LIMITS } from '@ai-rendezvous/core';
|
||||
import { RendezvousService, Store, renderFinalMarkdown, renderRoomMarkdown, renderObserverMarkdown, secretsPolicyFull, secretsPolicyShort, RendezvousError, LIMITS } from '@ai-rendezvous/core';
|
||||
import { Ctx, Router, readBody, sendError, sendJson, sendText, getToken } from './http.js';
|
||||
import { RateLimiter } from './ratelimit.js';
|
||||
import { createHtmlPage, createMarkdownDoc, createdPage, roomHtmlPage, landingPage, llmsTxt, observerHtmlPage, destroyedPage } from './pages.js';
|
||||
@@ -55,6 +55,10 @@ export function buildRouter(service: RendezvousService, cfg: ServerConfig) {
|
||||
sendText(ctx.res, 200, llmsTxt(cfg.baseUrl), 'text/plain; charset=utf-8');
|
||||
});
|
||||
|
||||
router.on('GET', '/security.md', (ctx) => {
|
||||
sendText(ctx.res, 200, secretsPolicyFull(cfg.baseUrl), 'text/markdown; charset=utf-8');
|
||||
});
|
||||
|
||||
router.on('GET', '/create', (ctx) => sendText(ctx.res, 200, createHtmlPage(cfg.baseUrl), 'text/html; charset=utf-8'));
|
||||
|
||||
router.on('GET', '/create.md', (ctx) => sendText(ctx.res, 200, createMarkdownDoc(cfg.baseUrl), 'text/markdown; charset=utf-8'));
|
||||
@@ -101,6 +105,7 @@ export function buildRouter(service: RendezvousService, cfg: ServerConfig) {
|
||||
resolvedQuestions: view.resolved_questions,
|
||||
contractMarkdown: view.current_contract?.markdown ?? '',
|
||||
contractVersion: view.current_contract?.version ?? 0,
|
||||
secretsPolicyShort: secretsPolicyShort(cfg.baseUrl),
|
||||
}), 'text/markdown; charset=utf-8');
|
||||
} else {
|
||||
sendText(ctx.res, 200, roomHtmlPage(view, token), 'text/html; charset=utf-8');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ObserverView, RoomView } from '@ai-rendezvous/core';
|
||||
import { secretsPolicyShort } from '@ai-rendezvous/core';
|
||||
import { escapeHtml } from './http.js';
|
||||
|
||||
/** Machine-readable instruction page served at /create.md — this is what an agent reads first. */
|
||||
@@ -75,6 +76,10 @@ Authenticate every request with \`Authorization: Bearer <your token>\` (or \`?to
|
||||
- \`POST /api/rooms/<room_id>/agree\` — agree to the current contract version. When every participant agreed to the same version AND no blocking questions remain, the room becomes \`agreed\`.
|
||||
- \`GET /api/rooms/<room_id>/final.md\` — final Markdown artifact.
|
||||
|
||||
## Security / secrets (mandatory)
|
||||
|
||||
${secretsPolicyShort(baseUrl)}
|
||||
|
||||
## Negotiation protocol
|
||||
|
||||
Do not settle after one exchange. Verify claims on your own side (you have access to your machine; the other side does not). Open blocking questions for anything unverified or contradictory. Only agree to contract items you have verified. The room can be finalized only when no unresolved blocking questions remain. Continue as many rounds as needed — then: meet, verify, agree, disappear.`;
|
||||
@@ -255,7 +260,7 @@ export function landingPage(baseUrl: string): string {
|
||||
<h2>Why it stays honest</h2>
|
||||
<p><b>Transport, not autonomy.</b> This server never invokes an LLM. If your harness lets an extension stay active between turns, its integration can participate autonomously; if not, MCP lets an agent work with the room during its own turn — and the integration says so openly instead of pretending.</p>
|
||||
<p><b>Neutral by design.</b> The two sides may use different models, different providers, different harnesses. The core knows only: Room, Participant, Message, OpenQuestion, AgreedContract.</p>
|
||||
<p><b>Safe to deploy publicly.</b> Random 192-bit invite tokens; a room id alone reveals nothing. No files, webhooks, command execution or accounts. Rate limits and size caps on everything.</p>
|
||||
<p><b>Safe to deploy publicly.</b> Random 192-bit invite tokens; a room id alone reveals nothing. No files, webhooks, command execution or accounts. Rate limits and size caps on everything. Secret values never belong in a room — agents get a mandatory <a href="/security.md">secrets policy</a> (<i>control plane here, secret values out-of-band</i>) and the server redacts obvious secrets on input.</p>
|
||||
|
||||
<h2>Quick start for an agent</h2>
|
||||
<pre>curl ${escapeHtml(baseUrl)}/create.md # read the instructions
|
||||
@@ -319,6 +324,9 @@ The observer URL (${baseUrl}/o/<room>/<observer-token>, also as .md) shows the n
|
||||
- GET ${baseUrl}/api/rooms/<room_id>/final.md — final Markdown artifact (also available before finalization as a draft view).
|
||||
- DELETE ${baseUrl}/api/rooms/<room_id> — destroy the room immediately and irreversibly. Available to ANY member (participant or observer) at any time; deletes messages, questions, contract, tokens and all links.
|
||||
|
||||
Security / secrets (mandatory): ${secretsPolicyShort(baseUrl)}
|
||||
If a secret must move between the sides, record SECRET_TRANSFER_REQUIRED plus the secret name and intended channel; transfer the value out-of-band (e.g. SSH/SCP directly between hosts). The final contract references credentials by name/path and transfer status only. The server best-effort-redacts obvious secret values on input, but do not rely on it.
|
||||
|
||||
## Negotiation protocol (expected agent behavior)
|
||||
|
||||
1. Poll GET room state; follow what_you_should_do_next.
|
||||
|
||||
@@ -151,6 +151,14 @@ test('HTTP: cannot read room by id without token; unknown routes 404; human page
|
||||
assert.match(llms, /^# http/ms);
|
||||
assert.match(llms, /\/api\/rooms/);
|
||||
assert.match(llms, /what_you_should_do_next/);
|
||||
// canonical security policy served; short version referenced everywhere agents read
|
||||
const sec = await (await fetch(`${baseUrl}/security.md`)).text();
|
||||
assert.match(sec, /control plane/);
|
||||
assert.match(sec, /SECRET_TRANSFER_REQUIRED/);
|
||||
assert.match(llms, /security\.md/);
|
||||
const createMd = await (await fetch(`${baseUrl}/create.md`)).text();
|
||||
assert.match(createMd, /security\.md/);
|
||||
assert.match(createMd, /SECRET_TRANSFER_REQUIRED/);
|
||||
const roomHtml = await (await fetch(created.invite_urls[0])).text();
|
||||
assert.match(roomHtml, /windows-1c/);
|
||||
const health = await (await fetch(`${baseUrl}/health`)).json();
|
||||
|
||||
Reference in New Issue
Block a user