Lock recovery hatches in dispatched turns
This commit is contained in:
parent
6579c6004d
commit
f0661a8995
8 changed files with 101 additions and 0 deletions
|
|
@ -80,6 +80,7 @@ For the full staged roadmap, including contribution points, revenue share, patro
|
|||
- `tools/governance-threads.mjs` uses that same bridge configuration to record `/governance/receipts` entries for topic opens, comments, approvals, and promotions
|
||||
- those agent activity scripts now fail closed without the bridge URL/token pair; `--allow-unreceipted-activity true` or `BIFROST_ALLOW_UNRECEIPTED_ACTIVITY=true` is the explicit operator-recovery hatch
|
||||
- GitHub bridge mutations now fail closed without those values; `--allow-ungated-github true` is reserved for explicit operator recovery
|
||||
- dispatched Codex turns now lock those recovery hatches too: local-only `--allow-ungated-github` and `--allow-unreceipted-activity` escapes are refused inside dispatched work so a worker cannot silently downgrade itself out of Bifrost receipts
|
||||
- set `BIFROST_REDDIT_CLIENT_ID`, `BIFROST_REDDIT_REFRESH_TOKEN`, and optionally `BIFROST_REDDIT_CLIENT_SECRET` before posting Reddit organizing threads
|
||||
- set `Heimdall:PatronSupportIntakeSecret` before enabling Heimdall patron support intake in production
|
||||
- default connection string points at local PostgreSQL
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ node .\tools\agent-transport.mjs apply-snapshot --in E:\tmp\bifrost-agent-transp
|
|||
Dispatch receipts should use Bifrost's own public persona in the Bifrost governance channel. `tools/dispatch-agent-requests.mjs` reads `BIFROST_DISCORD_CHANNEL_ID` or `DISCORD_BIFROST_CHANNEL_ID` for the receipt target, uses persona name `Bifrost`, and defaults the persona avatar to the public `src/Bifrost.Web/wwwroot/img/bifrost-profile.png` raw GitHub URL unless `BIFROST_DISCORD_PERSONA_AVATAR_URL` or `DISCORD_PERSONA_AVATAR_URL_BIFROST` overrides it. Receipt text must lead with the concrete repo and work title, not a generic "recent consensus" summary, and must not expose request ids, workspace paths, log paths, or other debugging debris in Discord.
|
||||
The dispatcher now tags those Discord receipts with the Bifrost request id as bridge provenance, and dispatched Codex turns inherit default bridge provenance environment variables so any explicit `tools/bifrost-bridge.mjs` crossing inside the turn can land on the same request trail without hand-carried flags. When `BIFROST_BRIDGE_BASE_URL` and `BIFROST_BRIDGE_TOKEN` are configured, the dispatcher also records `/dispatch/runs/*` receipts in the hosted app so Bifrost keeps a first-class run ledger for launch, completion, cancellation, and failure.
|
||||
Those dispatched turns also inherit Bifrost-owned GitHub gates through environment config. `git` and `gh` wrappers are prepended in `PATH`, a `pre-push` hook backs up raw Git pushes, raw `git push` stays blocked inside dispatched work even with `--no-verify`, and mutating `gh` commands are blocked too unless the bridge explicitly authorizes the GitHub mutation it owns.
|
||||
Those dispatched turns also lock the local-only recovery hatches. `--allow-ungated-github`, `BIFROST_ALLOW_UNGATED_GITHUB`, `--allow-unreceipted-activity`, and `BIFROST_ALLOW_UNRECEIPTED_ACTIVITY` are refused inside dispatched work so an agent cannot quietly downgrade itself out of Bifrost's gate or receipt trail.
|
||||
When that same bridge configuration is present, `tools/agent-transport.mjs` records `/transport/receipts` entries for queue, claim, release, and close operations on the request lane. The packet still lives in CultCache. The hosted receipt row is the Bifrost-side witness that an agent or dispatcher touched it.
|
||||
`apply-snapshot` is treated as a mutating import path too. It now fails closed without that same bridge configuration and derives request-lane receipts from the imported state delta, so a snapshot can no longer rewrite queue or claim state behind Bifrost's back.
|
||||
Those mutating scripts now fail closed without that bridge configuration. Use `--allow-unreceipted-activity true` or `BIFROST_ALLOW_UNRECEIPTED_ACTIVITY=true` only for explicit local recovery work where you are intentionally accepting a blind spot.
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ Current CLI wiring:
|
|||
- pass `--source-kind`, `--source-id`, `--authority-ref`, `--work-item-id`, or `--motion-id` so agent actions satisfy policy
|
||||
- the dispatcher now preloads `BIFROST_BRIDGE_SOURCE_KIND`, `BIFROST_BRIDGE_SOURCE_ID`, and `BIFROST_BRIDGE_AUTHORITY_REF` into Codex turns launched from Bifrost update requests, so normal bridge use inside a dispatched turn carries request provenance by default
|
||||
- those dispatched turns also inject Bifrost-owned GitHub gates through environment config: `git` and `gh` wrappers are prepended in `PATH`, a `pre-push` hook backs up raw Git pushes, raw `git push` stays blocked even when `--no-verify` is used, and mutating `gh` commands are blocked unless the bridge explicitly authorizes them
|
||||
- those dispatched turns also lock local-only recovery hatches: `--allow-ungated-github`, `BIFROST_ALLOW_UNGATED_GITHUB`, `--allow-unreceipted-activity`, and `BIFROST_ALLOW_UNRECEIPTED_ACTIVITY` are refused inside dispatched work so a worker cannot opt itself out of Bifrost gate or receipt policy
|
||||
- the dispatcher also uses that same bridge configuration to post dispatch-run start/complete/fail receipts, including the worker pid, thread/turn ids when available, and result/log paths for the local operator trail
|
||||
- the agent transport CLI uses that same bridge configuration to post request-lane receipts for queue, claim, release, and close events, so Bifrost does not go blind between intake and dispatch
|
||||
- the governance threads CLI uses that same bridge configuration to post governance receipts for topic opens, comments, approvals, and promotions, so Bifrost does not lose the pre-dispatch reasoning trail
|
||||
|
|
|
|||
|
|
@ -44,6 +44,28 @@ public sealed class BridgeCliTests
|
|||
Assert.True(payload.RootElement.GetProperty("dryRun").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GitHub_bridge_recovery_hatch_is_rejected_inside_dispatched_turn()
|
||||
{
|
||||
var result = await RunNodeAsync([
|
||||
"tools/bifrost-bridge.mjs",
|
||||
"github-pr-comment",
|
||||
"--repo-root", RepoRoot,
|
||||
"--identity", "nibu",
|
||||
"--pr", "1",
|
||||
"--content", "test comment",
|
||||
"--target-repository-full-name", "GameCult/Bifrost",
|
||||
"--allow-ungated-github", "true",
|
||||
"--dry-run", "true",
|
||||
], new Dictionary<string, string?>
|
||||
{
|
||||
["BIFROST_LOCK_RECOVERY_HATCHES"] = "true",
|
||||
});
|
||||
|
||||
Assert.NotEqual(0, result.ExitCode);
|
||||
Assert.Contains("cannot use --allow-ungated-github", result.Stderr, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GitHub_pr_comment_returns_concrete_github_receipt()
|
||||
{
|
||||
|
|
@ -118,6 +140,29 @@ console.log(JSON.stringify({
|
|||
Assert.Equal("queued", payload.RootElement.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Agent_transport_recovery_hatch_is_rejected_inside_dispatched_turn()
|
||||
{
|
||||
var storePath = Path.Combine(Path.GetTempPath(), $"bifrost-agent-transport-{Guid.NewGuid():N}.cc");
|
||||
var result = await RunNodeAsync([
|
||||
"tools/agent-transport.mjs",
|
||||
"enqueue",
|
||||
"--repo", "Bifrost",
|
||||
"--agent", "nibu",
|
||||
"--title", "Receipt gate recovery",
|
||||
"--request", "Do the thing.",
|
||||
"--store", storePath,
|
||||
"--allow-unmirrored", "true",
|
||||
"--allow-unreceipted-activity", "true",
|
||||
], new Dictionary<string, string?>
|
||||
{
|
||||
["BIFROST_LOCK_RECOVERY_HATCHES"] = "true",
|
||||
});
|
||||
|
||||
Assert.NotEqual(0, result.ExitCode);
|
||||
Assert.Contains("cannot use --allow-unreceipted-activity", result.Stderr, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Agent_transport_apply_snapshot_fails_closed_without_bifrost_receipt_gate()
|
||||
{
|
||||
|
|
@ -244,6 +289,29 @@ console.log(JSON.stringify({
|
|||
Assert.Equal("open", payload.RootElement.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Governance_recovery_hatch_is_rejected_inside_dispatched_turn()
|
||||
{
|
||||
var storePath = Path.Combine(Path.GetTempPath(), $"bifrost-governance-{Guid.NewGuid():N}.cc");
|
||||
var result = await RunNodeAsync([
|
||||
"tools/governance-threads.mjs",
|
||||
"open",
|
||||
"--repo", "Bifrost",
|
||||
"--agent", "nibu",
|
||||
"--title", "Governance receipt recovery",
|
||||
"--summary", "A topic body.",
|
||||
"--store", storePath,
|
||||
"--allow-unmirrored", "true",
|
||||
"--allow-unreceipted-activity", "true",
|
||||
], new Dictionary<string, string?>
|
||||
{
|
||||
["BIFROST_LOCK_RECOVERY_HATCHES"] = "true",
|
||||
});
|
||||
|
||||
Assert.NotEqual(0, result.ExitCode);
|
||||
Assert.Contains("cannot use --allow-unreceipted-activity", result.Stderr, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Dispatch_worker_fails_closed_without_bifrost_receipt_gate()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -137,6 +137,13 @@ function isMutatingCommand(command) {
|
|||
}
|
||||
|
||||
function ensureTransportReceiptGate(options) {
|
||||
if (wantsUnreceiptedActivity(options) && process.env.BIFROST_LOCK_RECOVERY_HATCHES === "true") {
|
||||
throw new Error(
|
||||
"Dispatched Bifrost work cannot use --allow-unreceipted-activity or BIFROST_ALLOW_UNRECEIPTED_ACTIVITY. " +
|
||||
"Request-lane mutations from a dispatched turn must keep Bifrost receipts.",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasBifrostBridgeConfig() || allowsUnreceiptedActivity(options)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -152,6 +159,10 @@ function hasBifrostBridgeConfig() {
|
|||
}
|
||||
|
||||
function allowsUnreceiptedActivity(options) {
|
||||
return wantsUnreceiptedActivity(options);
|
||||
}
|
||||
|
||||
function wantsUnreceiptedActivity(options) {
|
||||
return options["allow-unreceipted-activity"] === "true" || process.env.BIFROST_ALLOW_UNRECEIPTED_ACTIVITY === "true";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1036,6 +1036,13 @@ function ensureGitHubBridgeGate(options) {
|
|||
options["allow-ungated-github"] === "true" ||
|
||||
process.env.BIFROST_ALLOW_UNGATED_GITHUB === "true";
|
||||
|
||||
if (allowUngated && process.env.BIFROST_LOCK_RECOVERY_HATCHES === "true") {
|
||||
throw new Error(
|
||||
"Dispatched Bifrost work cannot use --allow-ungated-github or BIFROST_ALLOW_UNGATED_GITHUB. " +
|
||||
"GitHub mutations from a dispatched turn must go through the normal Bifrost gate and receipt path.",
|
||||
);
|
||||
}
|
||||
|
||||
if (baseUrl && token) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -567,6 +567,7 @@ function buildBridgeContextEnv(request) {
|
|||
BIFROST_BRIDGE_SOURCE_ID: request.id,
|
||||
BIFROST_BRIDGE_AUTHORITY_REF: "bifrost_dispatch_execution",
|
||||
BIFROST_ENFORCE_GITHUB_GATE: "true",
|
||||
BIFROST_LOCK_RECOVERY_HATCHES: "true",
|
||||
BIFROST_GIT_EXECUTABLE: resolve(gitGatePath, "git.cmd"),
|
||||
BIFROST_GH_EXECUTABLE: resolve(gitGatePath, "gh.cmd"),
|
||||
BIFROST_NODE_EXECUTABLE: process.execPath,
|
||||
|
|
|
|||
|
|
@ -533,6 +533,13 @@ function isMutatingCommand(command) {
|
|||
}
|
||||
|
||||
function ensureGovernanceReceiptGate(options) {
|
||||
if (wantsUnreceiptedActivity(options) && process.env.BIFROST_LOCK_RECOVERY_HATCHES === "true") {
|
||||
throw new Error(
|
||||
"Dispatched Bifrost work cannot use --allow-unreceipted-activity or BIFROST_ALLOW_UNRECEIPTED_ACTIVITY. " +
|
||||
"Governance mutations from a dispatched turn must keep Bifrost receipts.",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasBifrostBridgeConfig() || allowsUnreceiptedActivity(options)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -548,6 +555,10 @@ function hasBifrostBridgeConfig() {
|
|||
}
|
||||
|
||||
function allowsUnreceiptedActivity(options) {
|
||||
return wantsUnreceiptedActivity(options);
|
||||
}
|
||||
|
||||
function wantsUnreceiptedActivity(options) {
|
||||
return options["allow-unreceipted-activity"] === "true" || process.env.BIFROST_ALLOW_UNRECEIPTED_ACTIVITY === "true";
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue