Skip to content

ADR-0051: a connector principal runs the scheduled prompts its server declares - #1641

Draft
mgoldsborough wants to merge 5 commits into
mainfrom
scheduled-prompts-adr
Draft

mgoldsborough wants to merge 5 commits into
mainfrom
scheduled-prompts-adr

Conversation

@mgoldsborough

@mgoldsborough mgoldsborough commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What: ADR-0051 (Proposed), docs only.

  • A server declares scheduled prompts: ordinary MCP prompts marked _meta["ai.nimblebrain/scheduled-prompts"], carrying a schedule, the server's own tools, limits, a budget, and its needs in words. A declaration never names another server.
  • A workspace admin binds those needs to installed connectors and approves the bounds at install.
  • A connector principal (the installed connector acting as itself) owns and runs the resulting tasks. It has standing while the connector is installed, workspace reach only, and no identity tools or personal connectors.
  • A run calls prompts/get at each run, so the procedure updates with the server. Widened bounds wait for approval.
  • The runtime reconciles on connect, on list_changed and on boot, and never deletes a task. It holds a task whose prompt was withdrawn or that waits on an admin, and it lifts its own hold when the cause clears. An uninstalled connector's runs are skipped at the door. Admin overrides survive a reconcile, and a delete suppresses the task.

Why: a server's standing work otherwise runs as whichever member set it up by hand, and it drifts from the server's releases.

Test: docs only.

@mgoldsborough

Copy link
Copy Markdown
Contributor Author

QA Review: scheduled-prompts-adr

Scope: 1 file, +192/−0 (docs only) · worktree: detached at the head (the branch is checked out in its work folder) · reviewed: ab7acc5e
Risk: MEDIUM
Why this exists: a server whose value depends on standing work (one declarant exists today) has that work hand-provisioned per workspace from a pasted template. Releases never reach the copies, and an uninstall leaves tasks that fail. The ADR makes the server declare the work and the installed connector own it.

Necessity holds. Two pains are named, and the one alternative that avoids a new actor (the installer owns the tasks) is rejected for a reason ADR-0007 already establishes. Both Criticals below are in the reconcile rules, the part an implementer will copy literally.

Critical (must fix)

  • r1c1 [adr/0048…:112-128, :73-74] A task the runtime disables has no way back, and the reconcile cannot tell its own disable from an admin's pause [reasoned]. The reconcile rules only create, narrow, and disable. "Creates the task for an approved prompt that has none" never fires again once a disabled task exists. Three paths re-enable nothing:

    1. Reinstall. Uninstall disables every task (:120). A reinstall finds them present and leaves them disabled. That contradicts :73-74, which models the uninstalled case on ADR-0007's removed member, whose run is recorded skipped so that it self-heals. With the disable in place, the door check at :73 never fires, and nothing self-heals.
    2. Withdrawal reversed. A release drops or unmarks a prompt, then the next release restores it. The task stays disabled in every workspace until an admin finds it.
    3. Ambiguity. "Disabled by reconcile" and "paused by an admin (override)" are the same visible state. A reconcile that tried to re-enable would undo the admin's pause, which :125-126 forbids.
      · Preconditions: connector uninstalled → reinstalled (or prompt withdrawn → restored) → reconcile sees an existing disabled task → no rule applies → the work silently never runs.
      · Fix: make the runtime's disable a reconcile-owned state, separate from the admin override (for example, withdrawn or uninstalled as a reason), and add the inverse rule: "it re-enables a task it disabled when the prompt is approved and present again; it never re-enables one an admin paused or deleted." Or drop the disable-on-uninstall line, rely on the door's skip (:73-74), and keep disable only for withdrawal, with the same inverse.
  • r1c2 [adr/0048…:55, :89-90, :119] A release that changes a prompt's arguments is unaddressed, and the -32602 rule turns it into a fleet-wide disable [reasoned]. The approval records "the values" for the prompt's arguments, and prompts/get is called with those approved values at each run. MCP's prompts error table assigns -32602 to both "invalid prompt name" and "missing required arguments". So a release that adds a required argument makes every workspace's next run answer -32602. Under :119 that reads as "withdrawn", and the task is disabled, with no approval asked for the new value. The widening list at :93-95 (tools, schedule, limits, budget) doesn't mention arguments either way.
    · Preconditions: server release adds a required prompt argument → approved values lack it → prompts/get → -32602 → reconcile treats it as withdrawn → disabled (and, per r1c1, stays disabled).
    · Fix: make the argument set part of what is compared at reconcile. A new required argument waits for approval, like a widening, with the old values kept until the admin supplies the new one. Treat a removed argument as narrowing. Decide "withdrawn" from prompts/list at reconcile only, not from a run-time -32602, which cannot tell a missing argument from a missing prompt. At run time, fail that run as poor or failed and let reconcile decide.

Fix in-PR (apply mechanically — no adjudication essay expected)

  • r1f1 [adr/0048…:96-99] State what a task does while one of its needs is unbound (bound connector uninstalled, admin not yet answered): it pauses, or it runs without that reach. One sentence. As written, it keeps running a procedure whose need is unmet, unattended.
  • r1f2 [adr/0048…:151-153] The consequence presents "which tools act on the workspace vs a person" as a new property every platform tool must state. The partition largely exists already. Identity-scoped sources have one authority (src/tools/identity-sources.ts), the caller- and org-acting kernel tools carry ADR-0043's mark, and the org-administration tools are app-only. Name those as the carriers, and state only what is new (if anything), rather than a fresh per-tool property.
  • r1f3 [adr/0048…:76] Rewrap the line (124 columns). Every other line is ≤ 80.
  • r1f4 PR body: refresh it. It still says the marker carries "requires for other tools". Since ab7acc5e the marker carries needs in words, never names another server's tools, and the workspace binds them.

Suggestions (optional)

  • r1s1 [:113-114] Reconcile is "keyed by workspace, server and prompt name", but the principal is per install (:66-68), and :62-63 argues that names break on rename. Keying by the install keeps a server rename from orphaning the old tasks and creating unapproved new ones.
  • r1s2 [:92-95] Say how a declaration that narrows one dimension and widens another is treated. Presumably the whole declaration waits, and the approved bounds keep running.
  • r1s3 [:43-45] Place the marker in ADR-0024's two-kind rule explicitly: it is accepted from the wire because it widens nothing until an admin approves it. Otherwise it reads as a widening _meta key that ADR-0024 says must be host-owned.

What Looks Good

  • The reach model is tight. The principal gets no identity tools and no personal connectors, uses only workspace credentials, and is charged to one account per install. The event-trigger refusal rests on a real allowlist (EVENT_SCHEDULE_SOURCES in src/platform/tasks/domain.ts, user and agent only), so a connector provenance is refused by construction.
  • "A declaration never names another server" plus admin-bound needs closes the escalation where a release widens reach into other connectors. The Alternatives entry states the reason.
  • Consequences are honest about the residual: a procedure change is trusted within the bounds, and the runtime cannot judge whether it uses them well.
  • No ADR number collision (the open spend ADR is 0049). Status and Serves match the template.

Body claims

  • ✅ Docs only, no code: one added file.
  • ❌ "requires for other tools": superseded by needs (r1f4).
  • ✅ Reconciles on connect, list_changed, boot and uninstall, and never deletes: matches :112-122.
  • ✅ Overrides survive reconciles, and a delete suppresses: matches :124-128.
machine record
{
  "kind": "review", "pr": 1641, "round": 1,
  "reviewed": "ab7acc5e9f5580c4e7b2aaf7dd0f2a8c188367d2", "delta_base": null,
  "risk": "MEDIUM", "stop_gate": "n/a",
  "verdict": "Sound reach model; the reconcile rules lack a re-enable transition and do not handle prompt-argument changes.",
  "findings": [
    {"id": "r1c1", "bucket": "critical", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 112,
     "claim": "Reconcile-disabled tasks have no re-enable path (reinstall, restored prompt) and are indistinguishable from an admin pause; disable-on-uninstall also defeats the ADR-0007 self-heal the door check at :73 cites.",
     "evidence": "reasoned", "receipt": "read :66-128; reconcile rules are create/narrow/disable only; ADR-0007 skip exists so runs self-heal",
     "preconditions": "uninstall->reinstall or withdraw->restore -> existing disabled task -> no rule applies -> never runs",
     "fix": "reconcile-owned disable reason separate from override + inverse re-enable rule; or drop disable-on-uninstall and rely on the door skip", "scope": "core"},
    {"id": "r1c2", "bucket": "critical", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 119,
     "claim": "Prompt-argument changes are unaddressed; a new required argument yields -32602 at run time, which :119 treats as withdrawn and disables.",
     "evidence": "reasoned", "receipt": "MCP prompts error table: invalid name and missing required arguments both -32602; widening list :93-95 omits arguments",
     "preconditions": "release adds required argument -> approved values lack it -> prompts/get -32602 -> disabled",
     "fix": "compare the argument set at reconcile (new required arg = approval); decide withdrawal from prompts/list only", "scope": "core"},
    {"id": "r1f1", "bucket": "fix-in-pr", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 98,
     "claim": "Behaviour of a task while a need is unbound is unstated.", "evidence": "reasoned", "receipt": null, "preconditions": null, "fix": "one sentence: pause or run without", "scope": "core"},
    {"id": "r1f2", "bucket": "fix-in-pr", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 151,
     "claim": "Consequence presents a workspace-vs-person tool property as new; identity-sources.ts, the ADR-0043 mark and app-only visibility already carry it.",
     "evidence": "reasoned", "receipt": "read src/tools/identity-sources.ts header and adr/0043", "preconditions": null, "fix": "name existing carriers, state only what is new", "scope": "peripheral"},
    {"id": "r1f3", "bucket": "fix-in-pr", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 76,
     "claim": "Line is 124 columns; rest of file wraps at 80.", "evidence": "reproduced", "receipt": "awk length>82 -> only line 76 (124)", "preconditions": null, "fix": "rewrap", "scope": "peripheral"},
    {"id": "r1f4", "bucket": "fix-in-pr", "file": "PR body", "line": 0,
     "claim": "Body says the marker carries `requires` for other tools; the ADR now uses `needs` in words.", "evidence": "reproduced", "receipt": "gh pr view body vs ADR :49-64", "preconditions": null, "fix": "refresh the body", "scope": "peripheral"},
    {"id": "r1s1", "bucket": "suggestion", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 113,
     "claim": "Reconcile key uses server name while the principal is per install.", "evidence": "reasoned", "receipt": null, "preconditions": null, "fix": "key by install", "scope": "peripheral"},
    {"id": "r1s2", "bucket": "suggestion", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 92,
     "claim": "Mixed narrow+widen declarations unspecified.", "evidence": "reasoned", "receipt": null, "preconditions": null, "fix": "whole declaration waits", "scope": "peripheral"},
    {"id": "r1s3", "bucket": "suggestion", "file": "adr/0048-a-connector-principal-runs-the-scheduled-prompts-its-server-declares.md", "line": 43,
     "claim": "Marker's ADR-0024 kind is unstated.", "evidence": "reasoned", "receipt": null, "preconditions": null, "fix": "state it widens nothing until approved", "scope": "peripheral"}
  ]
}

Next: answer with pr-adjudicate.

@mgoldsborough mgoldsborough changed the title ADR-0048: a connector principal runs the scheduled prompts its server declares ADR-0051: a connector principal runs the scheduled prompts its server declares Oct 10, 2026
@mgoldsborough

Copy link
Copy Markdown
Contributor Author

Adjudication round 1: both Criticals valid, fixed with one runtime-owned hold · fixed at 097d585

Declined: r1s1 (keying reconcile by the install). The name key stays; a server rename is not a case this ADR has to carry.

r1c1 · valid

premise: verified — the reconcile rules at ab7acc5e only create, narrow and disable. Fixed: uninstall no longer disables (the door's skip covers it, and a reinstall lets the next run through). Withdrawal now holds the task, a runtime-owned state with a reason, kept apart from an admin's pause. The reconcile lifts its own hold when the cause clears.

r1c2 · valid

premise: verified — the spec's prompts error table gives -32602 to both an invalid name and missing arguments. Fixed: withdrawal is decided from prompts/list at reconcile only, and a run that fails is a failed run. A new required argument holds the task until the admin gives a value. A removed argument is a narrowing.

Fix in-PR

r1f1–r1f4 applied, 097d585. r1f1: a need the admin leaves unbound is a decision, and the run goes without that reach. A need unbound by an uninstall holds the task until the admin answers. r1f4: the body already said needs after the round-0 rewrite; its reconcile bullet is refreshed. r1s2 and r1s3 taken.

Verification

CI: deferred (draft; docs only) · local: awk 'length>80' clean, grep for disable, requires and leftover -32602 rules finds none, deletion side of the diff read · nothing executable changed.

machine record
{
  "kind": "adjudication", "pr": 1641, "round": 1, "fixed_at": "097d585a",
  "verdicts": [
    {"id": "r1c1", "verdict": "valid", "premise": "verified",
     "resolution": "no disable on uninstall (door skip); withdrawal holds the task with a runtime-owned reason apart from an admin pause; reconcile lifts its own hold when the cause clears; 097d585a"},
    {"id": "r1c2", "verdict": "valid", "premise": "verified",
     "resolution": "withdrawal decided from prompts/list at reconcile only; a run-time -32602 is a failed run; a new required argument holds until the admin gives a value; a removed argument narrows; 097d585a"},
    {"id": "r1f1", "verdict": "valid", "premise": "verified", "resolution": "applied, 097d585a"},
    {"id": "r1f2", "verdict": "valid", "premise": "verified", "resolution": "applied, 097d585a"},
    {"id": "r1f3", "verdict": "valid", "premise": "verified", "resolution": "applied, 097d585a"},
    {"id": "r1f4", "verdict": "valid", "premise": "verified", "resolution": "body refreshed"},
    {"id": "r1s1", "verdict": "valid", "premise": "accepted", "resolution": "declined"},
    {"id": "r1s2", "verdict": "valid", "premise": "accepted", "resolution": "taken, 097d585a"},
    {"id": "r1s3", "verdict": "valid", "premise": "accepted", "resolution": "taken, 097d585a"}
  ],
  "ci": "deferred",
  "local": "awk length>80 clean; grep for disable/requires/-32602 rules: none; deletion side read"
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant