switchroom 0.13.33 → 0.13.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/timezone-hook.sh +1 -1
- package/dist/agent-scheduler/index.js +8 -1
- package/dist/auth-broker/index.js +8 -1
- package/dist/cli/switchroom.js +176 -26
- package/dist/host-control/main.js +5222 -203
- package/dist/vault/approvals/kernel-server.js +9 -2
- package/dist/vault/broker/server.js +9 -2
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +234 -31
- package/telegram-plugin/docs/waiting-ux-spec.md +40 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +188 -1
- package/telegram-plugin/gateway/config-approval-handler.ts +170 -15
- package/telegram-plugin/gateway/diff-preview-card.test.ts +2 -2
- package/telegram-plugin/gateway/diff-preview-card.ts +2 -2
- package/telegram-plugin/gateway/drive-write-approval.test.ts +70 -0
- package/telegram-plugin/gateway/drive-write-approval.ts +51 -2
- package/telegram-plugin/gateway/error-envelope-card.ts +64 -0
- package/telegram-plugin/gateway/gateway.ts +112 -15
- package/telegram-plugin/gateway/ipc-protocol.ts +10 -1
- package/telegram-plugin/gateway/oversize-card-body.test.ts +108 -0
- package/telegram-plugin/gateway/oversize-card-body.ts +114 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +46 -1
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +118 -41
- package/telegram-plugin/hooks/silent-end-scan.mjs +190 -0
- package/telegram-plugin/pending-work-progress.ts +37 -1
- package/telegram-plugin/tests/boot-clears-clean-shutdown-marker.test.ts +75 -0
- package/telegram-plugin/tests/error-envelope-unlock-card.test.ts +79 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +134 -0
- package/telegram-plugin/tests/silent-end-integration.test.ts +268 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +242 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +314 -0
- package/telegram-plugin/tests/silent-end.test.ts +227 -38
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +51 -6
package/bin/timezone-hook.sh
CHANGED
|
@@ -45,7 +45,7 @@ fi
|
|
|
45
45
|
# `-d "@<unix>"` is required (Linux-only, fine for switchroom production).
|
|
46
46
|
NOW_UNIX=$(date +%s)
|
|
47
47
|
ROUNDED=$(( NOW_UNIX - (NOW_UNIX % 900) ))
|
|
48
|
-
NOW=$(TZ="$TZ_VAL" date -d "@$ROUNDED" '+%Y-%m-%d %
|
|
48
|
+
NOW=$(TZ="$TZ_VAL" date -d "@$ROUNDED" '+%A %Y-%m-%d %I:%M %p %Z (UTC%:z)')
|
|
49
49
|
|
|
50
50
|
if [ "$TZ_UNSET" = "1" ]; then
|
|
51
51
|
MSG="Current local time: $NOW ($TZ_VAL — WARNING: SWITCHROOM_TIMEZONE unset; compose env may be stale, run \`switchroom apply && switchroom agent restart <agent>\` to refresh)"
|
|
@@ -11302,8 +11302,15 @@ var QuotaConfigSchema = exports_external.object({
|
|
|
11302
11302
|
weekly_budget_usd: exports_external.number().positive().optional().describe("Weekly USD spend budget. If unset, the greeting shows raw usage only."),
|
|
11303
11303
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11304
11304
|
});
|
|
11305
|
+
var AutoReleaseCheckSchema = exports_external.object({
|
|
11306
|
+
enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false — opt-in."),
|
|
11307
|
+
interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
|
|
11308
|
+
apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
|
|
11309
|
+
image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
|
|
11310
|
+
});
|
|
11305
11311
|
var HostControlConfigSchema = exports_external.object({
|
|
11306
|
-
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
11312
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
|
|
11313
|
+
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
11307
11314
|
});
|
|
11308
11315
|
var HostdConfigSchema = exports_external.object({
|
|
11309
11316
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
|
@@ -11302,8 +11302,15 @@ var QuotaConfigSchema = exports_external.object({
|
|
|
11302
11302
|
weekly_budget_usd: exports_external.number().positive().optional().describe("Weekly USD spend budget. If unset, the greeting shows raw usage only."),
|
|
11303
11303
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11304
11304
|
});
|
|
11305
|
+
var AutoReleaseCheckSchema = exports_external.object({
|
|
11306
|
+
enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false — opt-in."),
|
|
11307
|
+
interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
|
|
11308
|
+
apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
|
|
11309
|
+
image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
|
|
11310
|
+
});
|
|
11305
11311
|
var HostControlConfigSchema = exports_external.object({
|
|
11306
|
-
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
11312
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
|
|
11313
|
+
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
11307
11314
|
});
|
|
11308
11315
|
var HostdConfigSchema = exports_external.object({
|
|
11309
11316
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
|
|
|
13520
13520
|
});
|
|
13521
13521
|
|
|
13522
13522
|
// src/config/schema.ts
|
|
13523
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
|
|
13523
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
|
|
13524
13524
|
var init_schema = __esm(() => {
|
|
13525
13525
|
init_zod();
|
|
13526
13526
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -13866,8 +13866,15 @@ var init_schema = __esm(() => {
|
|
|
13866
13866
|
weekly_budget_usd: exports_external.number().positive().optional().describe("Weekly USD spend budget. If unset, the greeting shows raw usage only."),
|
|
13867
13867
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
13868
13868
|
});
|
|
13869
|
+
AutoReleaseCheckSchema = exports_external.object({
|
|
13870
|
+
enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false \u2014 opt-in."),
|
|
13871
|
+
interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
|
|
13872
|
+
apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
|
|
13873
|
+
image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
|
|
13874
|
+
});
|
|
13869
13875
|
HostControlConfigSchema = exports_external.object({
|
|
13870
|
-
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
13876
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
|
|
13877
|
+
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
13871
13878
|
});
|
|
13872
13879
|
HostdConfigSchema = exports_external.object({
|
|
13873
13880
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
|
@@ -29101,7 +29108,7 @@ function decodeResponse3(line) {
|
|
|
29101
29108
|
const obj = JSON.parse(line);
|
|
29102
29109
|
return ResponseSchema3.parse(obj);
|
|
29103
29110
|
}
|
|
29104
|
-
var MAX_FRAME_BYTES3, RequestEnvelope, AgentRestartRequestSchema, UpgradeStatusRequestSchema, GetStatusRequestSchema, AgentNameSchema2, UpdateCheckRequestSchema, UpdateApplyRequestSchema, ApplyRequestSchema, AgentStartRequestSchema, AgentStopRequestSchema, AgentLogsRequestSchema, AgentExecRequestSchema, DoctorRequestSchema, AgentSmokeRequestSchema, ConfigProposeEditRequestSchema, RequestSchema3, ResultSchema, ResponseEnvelope, ResponseSchema3;
|
|
29111
|
+
var MAX_FRAME_BYTES3, RequestEnvelope, AgentRestartRequestSchema, UpgradeStatusRequestSchema, GetStatusRequestSchema, AgentNameSchema2, UpdateCheckRequestSchema, UpdateApplyRequestSchema, ApplyRequestSchema, AgentStartRequestSchema, AgentStopRequestSchema, AgentLogsRequestSchema, AgentExecRequestSchema, DoctorRequestSchema, AgentSmokeRequestSchema, ConfigProposeEditRequestSchema, RequestSchema3, ResultSchema, ErrorFixSchema, ErrorEnvelopeSchema, ResponseEnvelope, ResponseSchema3;
|
|
29105
29112
|
var init_protocol3 = __esm(() => {
|
|
29106
29113
|
init_zod();
|
|
29107
29114
|
MAX_FRAME_BYTES3 = 64 * 1024;
|
|
@@ -29220,6 +29227,45 @@ var init_protocol3 = __esm(() => {
|
|
|
29220
29227
|
ConfigProposeEditRequestSchema
|
|
29221
29228
|
]);
|
|
29222
29229
|
ResultSchema = exports_external.enum(["started", "completed", "denied", "error"]);
|
|
29230
|
+
ErrorFixSchema = exports_external.discriminatedUnion("kind", [
|
|
29231
|
+
exports_external.object({
|
|
29232
|
+
kind: exports_external.literal("flip_yaml_flag"),
|
|
29233
|
+
yaml_path: exports_external.string(),
|
|
29234
|
+
to: exports_external.unknown()
|
|
29235
|
+
}),
|
|
29236
|
+
exports_external.object({
|
|
29237
|
+
kind: exports_external.literal("request_vault_grant"),
|
|
29238
|
+
vault_key: exports_external.string()
|
|
29239
|
+
}),
|
|
29240
|
+
exports_external.object({
|
|
29241
|
+
kind: exports_external.literal("operator_action"),
|
|
29242
|
+
subkind: exports_external.enum(["policy_denied", "infra", "out_of_scope"]),
|
|
29243
|
+
operator_steps: exports_external.array(exports_external.string()).min(1).optional()
|
|
29244
|
+
}),
|
|
29245
|
+
exports_external.object({
|
|
29246
|
+
kind: exports_external.literal("retry_after"),
|
|
29247
|
+
retry_at: exports_external.string()
|
|
29248
|
+
}),
|
|
29249
|
+
exports_external.object({
|
|
29250
|
+
kind: exports_external.literal("quota_exceeded"),
|
|
29251
|
+
quota: exports_external.string(),
|
|
29252
|
+
current: exports_external.number(),
|
|
29253
|
+
limit: exports_external.number()
|
|
29254
|
+
}),
|
|
29255
|
+
exports_external.object({
|
|
29256
|
+
kind: exports_external.literal("bad_input"),
|
|
29257
|
+
field: exports_external.string().optional()
|
|
29258
|
+
})
|
|
29259
|
+
]);
|
|
29260
|
+
ErrorEnvelopeSchema = exports_external.object({
|
|
29261
|
+
v: exports_external.literal(1),
|
|
29262
|
+
code: exports_external.string().regex(/^(E_[A-Z0-9_]+|VAULT-[A-Z-]+)$/),
|
|
29263
|
+
human: exports_external.string().min(1),
|
|
29264
|
+
why: exports_external.string().optional(),
|
|
29265
|
+
fix: ErrorFixSchema.optional(),
|
|
29266
|
+
docs: exports_external.string().url().optional(),
|
|
29267
|
+
request_id: exports_external.string().min(1)
|
|
29268
|
+
});
|
|
29223
29269
|
ResponseEnvelope = {
|
|
29224
29270
|
v: exports_external.literal(1),
|
|
29225
29271
|
request_id: exports_external.string().min(1).max(128),
|
|
@@ -29229,7 +29275,8 @@ var init_protocol3 = __esm(() => {
|
|
|
29229
29275
|
audit_id: exports_external.string().min(1).optional(),
|
|
29230
29276
|
stdout_tail: exports_external.string().optional(),
|
|
29231
29277
|
stderr_tail: exports_external.string().optional(),
|
|
29232
|
-
error: exports_external.string().optional()
|
|
29278
|
+
error: exports_external.string().optional(),
|
|
29279
|
+
error_envelope: ErrorEnvelopeSchema.optional()
|
|
29233
29280
|
};
|
|
29234
29281
|
ResponseSchema3 = exports_external.object(ResponseEnvelope);
|
|
29235
29282
|
});
|
|
@@ -47403,10 +47450,18 @@ async function dispatchTool2(name, args) {
|
|
|
47403
47450
|
if (resp.result === "started" || resp.result === "completed") {
|
|
47404
47451
|
return jsonText2(resp);
|
|
47405
47452
|
}
|
|
47406
|
-
|
|
47407
|
-
|
|
47408
|
-
|
|
47409
|
-
|
|
47453
|
+
const content = [
|
|
47454
|
+
{ type: "text", text: JSON.stringify(resp) }
|
|
47455
|
+
];
|
|
47456
|
+
if (resp.error_envelope) {
|
|
47457
|
+
const env2 = resp.error_envelope;
|
|
47458
|
+
content.push({
|
|
47459
|
+
type: "text",
|
|
47460
|
+
text: `Structured error \u2014 fix.kind=${env2.fix?.kind ?? "none"}
|
|
47461
|
+
` + JSON.stringify(env2, null, 2)
|
|
47462
|
+
});
|
|
47463
|
+
}
|
|
47464
|
+
return { content, isError: true };
|
|
47410
47465
|
}
|
|
47411
47466
|
function resolveAuditLogPath() {
|
|
47412
47467
|
if (process.env.HOSTD_AUDIT_LOG_PATH)
|
|
@@ -47656,8 +47711,8 @@ var {
|
|
|
47656
47711
|
} = import__.default;
|
|
47657
47712
|
|
|
47658
47713
|
// src/build-info.ts
|
|
47659
|
-
var VERSION = "0.13.
|
|
47660
|
-
var COMMIT_SHA = "
|
|
47714
|
+
var VERSION = "0.13.36";
|
|
47715
|
+
var COMMIT_SHA = "73e8bb05";
|
|
47661
47716
|
|
|
47662
47717
|
// src/cli/agent.ts
|
|
47663
47718
|
init_source();
|
|
@@ -49707,17 +49762,16 @@ function classifyChange(path, agentDir, useHotReloadStable) {
|
|
|
49707
49762
|
return "stale-till-restart";
|
|
49708
49763
|
}
|
|
49709
49764
|
function buildSettingsHooksBlock(p) {
|
|
49710
|
-
const { agentName, agentConfig, hindsightEnabled, useSwitchroomPlugin
|
|
49765
|
+
const { agentName, agentConfig, hindsightEnabled, useSwitchroomPlugin } = p;
|
|
49711
49766
|
const userHooks = translateHooksToClaudeShape(agentConfig.hooks);
|
|
49712
49767
|
const wrapper = `bash "${join8(DOCKER_BIN_PATH, "run-hook.sh")}"`;
|
|
49713
49768
|
const wrap = (source, command) => `${wrapper} ${shellSingleQuote(source)} ${command}`;
|
|
49714
49769
|
const handoffEnabled = agentConfig.session_continuity?.enabled !== false;
|
|
49715
|
-
const handoffConfigArg = configPath ? ` --config ${shellSingleQuote(DOCKER_CONFIG_PATH)}` : "";
|
|
49716
49770
|
const stopHooks = [];
|
|
49717
49771
|
if (handoffEnabled) {
|
|
49718
49772
|
stopHooks.push({
|
|
49719
49773
|
type: "command",
|
|
49720
|
-
command: wrap("hook:handoff", `switchroom
|
|
49774
|
+
command: wrap("hook:handoff", `switchroom handoff ${agentName}`),
|
|
49721
49775
|
timeout: 35,
|
|
49722
49776
|
async: true
|
|
49723
49777
|
});
|
|
@@ -61526,6 +61580,23 @@ function registerVaultBackupCommand(vault, program3) {
|
|
|
61526
61580
|
});
|
|
61527
61581
|
}
|
|
61528
61582
|
|
|
61583
|
+
// src/cli/vault-denied-envelope.ts
|
|
61584
|
+
var ENVELOPE_SENTINEL = "ERROR-ENVELOPE:";
|
|
61585
|
+
function writeVaultDeniedEnvelope(vaultKey, brokerCode, human) {
|
|
61586
|
+
const envelope = {
|
|
61587
|
+
v: 1,
|
|
61588
|
+
code: "VAULT-BROKER-DENIED",
|
|
61589
|
+
human: `${brokerCode}: ${human}`,
|
|
61590
|
+
fix: {
|
|
61591
|
+
kind: "request_vault_grant",
|
|
61592
|
+
vault_key: vaultKey
|
|
61593
|
+
},
|
|
61594
|
+
request_id: `vault-cli-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
61595
|
+
};
|
|
61596
|
+
process.stderr.write(`${ENVELOPE_SENTINEL} ${JSON.stringify(envelope)}
|
|
61597
|
+
`);
|
|
61598
|
+
}
|
|
61599
|
+
|
|
61529
61600
|
// src/cli/vault.ts
|
|
61530
61601
|
function isSandboxContext() {
|
|
61531
61602
|
return process.env.SWITCHROOM_RUNTIME === "docker";
|
|
@@ -61751,6 +61822,7 @@ function registerVaultCommand(program3) {
|
|
|
61751
61822
|
}
|
|
61752
61823
|
process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
|
|
61753
61824
|
`);
|
|
61825
|
+
writeVaultDeniedEnvelope(key, result.code, result.msg);
|
|
61754
61826
|
process.exit(VAULT_EXIT_DENIED);
|
|
61755
61827
|
}
|
|
61756
61828
|
if (inSandbox) {
|
|
@@ -61966,6 +62038,7 @@ Push passphrase to broker for future requests? [Y/n]: `);
|
|
|
61966
62038
|
process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
|
|
61967
62039
|
` + `${recoveryHint("denied", key)}
|
|
61968
62040
|
`);
|
|
62041
|
+
writeVaultDeniedEnvelope(key, result.code, result.msg);
|
|
61969
62042
|
process.exit(2);
|
|
61970
62043
|
}
|
|
61971
62044
|
} else {
|
|
@@ -70736,21 +70809,32 @@ init_loader();
|
|
|
70736
70809
|
import { resolve as resolve33 } from "node:path";
|
|
70737
70810
|
function registerHandoffCommand(program3) {
|
|
70738
70811
|
program3.command("handoff <agent>", { hidden: true }).description("Build the agent's session handoff sidecars \u2014 a transcript-tail " + "briefing (.handoff.md) and topic line (.handoff-topic). " + "[internal \u2014 used by the Stop hook]").option("--max-turns <n>", "Max turns kept in the handoff transcript tail", String(DEFAULT_MAX_TURNS)).action(withConfigError(async (agentName, opts) => {
|
|
70739
|
-
|
|
70740
|
-
|
|
70741
|
-
|
|
70742
|
-
|
|
70812
|
+
let agentConfig;
|
|
70813
|
+
let agentDir;
|
|
70814
|
+
try {
|
|
70815
|
+
const config = getConfig(program3);
|
|
70816
|
+
agentConfig = config.agents[agentName];
|
|
70817
|
+
if (!agentConfig) {
|
|
70818
|
+
process.stderr.write(`handoff: agent "${agentName}" not defined in switchroom.yaml
|
|
70743
70819
|
`);
|
|
70744
|
-
|
|
70820
|
+
return;
|
|
70821
|
+
}
|
|
70822
|
+
const agentsDir = resolveAgentsDir(config);
|
|
70823
|
+
agentDir = resolve33(agentsDir, agentName);
|
|
70824
|
+
} catch (err) {
|
|
70825
|
+
if (!(err instanceof ConfigError))
|
|
70826
|
+
throw err;
|
|
70827
|
+
process.stderr.write(`handoff: yaml unavailable (${err.message}); using defaults
|
|
70828
|
+
`);
|
|
70829
|
+
agentConfig = undefined;
|
|
70830
|
+
agentDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
70745
70831
|
}
|
|
70746
|
-
const continuity = agentConfig
|
|
70832
|
+
const continuity = agentConfig?.session_continuity;
|
|
70747
70833
|
if (continuity?.enabled === false) {
|
|
70748
70834
|
process.stderr.write(`handoff: session_continuity.enabled=false for "${agentName}"; skipping
|
|
70749
70835
|
`);
|
|
70750
70836
|
return;
|
|
70751
70837
|
}
|
|
70752
|
-
const agentsDir = resolveAgentsDir(config);
|
|
70753
|
-
const agentDir = resolve33(agentsDir, agentName);
|
|
70754
70838
|
const claudeConfigDir = resolve33(agentDir, ".claude");
|
|
70755
70839
|
const jsonl = findLatestSessionJsonl(claudeConfigDir);
|
|
70756
70840
|
if (!jsonl) {
|
|
@@ -76133,7 +76217,34 @@ function denyPendingScheduleEntry(opts) {
|
|
|
76133
76217
|
|
|
76134
76218
|
// src/cli/agent-config-write.ts
|
|
76135
76219
|
import { existsSync as existsSync73, readFileSync as readFileSync59 } from "node:fs";
|
|
76220
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
76136
76221
|
var MAX_ENTRIES_PER_AGENT = 20;
|
|
76222
|
+
var MIN_CRON_INTERVAL_MIN = 5;
|
|
76223
|
+
function extractCronSmallestGapMin(expr) {
|
|
76224
|
+
const fields = expr.trim().split(/\s+/);
|
|
76225
|
+
if (fields.length < 5)
|
|
76226
|
+
return 0;
|
|
76227
|
+
const min = fields[0];
|
|
76228
|
+
if (min === "*")
|
|
76229
|
+
return 1;
|
|
76230
|
+
const step = min.match(/^\*\/(\d+)$/);
|
|
76231
|
+
if (step)
|
|
76232
|
+
return Number(step[1]);
|
|
76233
|
+
if (min.includes(",")) {
|
|
76234
|
+
const parts = min.split(",").map((s) => Number(s)).filter((n) => Number.isFinite(n));
|
|
76235
|
+
if (parts.length >= 2) {
|
|
76236
|
+
const sorted = [...parts].sort((a, b) => a - b);
|
|
76237
|
+
let smallest = Infinity;
|
|
76238
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
76239
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
76240
|
+
if (gap > 0 && gap < smallest)
|
|
76241
|
+
smallest = gap;
|
|
76242
|
+
}
|
|
76243
|
+
return Number.isFinite(smallest) ? smallest : 0;
|
|
76244
|
+
}
|
|
76245
|
+
}
|
|
76246
|
+
return 0;
|
|
76247
|
+
}
|
|
76137
76248
|
function checkOperatorContext(verb, env2 = process.env) {
|
|
76138
76249
|
if (env2.SWITCHROOM_OPERATOR === "1")
|
|
76139
76250
|
return { ok: true };
|
|
@@ -76146,8 +76257,45 @@ function checkOperatorContext(verb, env2 = process.env) {
|
|
|
76146
76257
|
}
|
|
76147
76258
|
return { ok: true };
|
|
76148
76259
|
}
|
|
76260
|
+
function buildEnvelopeForCode(code, message, extra) {
|
|
76261
|
+
const request_id = `agent-config-${randomUUID5()}`;
|
|
76262
|
+
if (code === "E_CRON_TOO_FREQUENT") {
|
|
76263
|
+
return {
|
|
76264
|
+
v: 1,
|
|
76265
|
+
code,
|
|
76266
|
+
human: message,
|
|
76267
|
+
fix: {
|
|
76268
|
+
kind: "quota_exceeded",
|
|
76269
|
+
quota: "cron_min_interval_minutes",
|
|
76270
|
+
current: typeof extra.requested_interval_min === "number" ? extra.requested_interval_min : 0,
|
|
76271
|
+
limit: MIN_CRON_INTERVAL_MIN
|
|
76272
|
+
},
|
|
76273
|
+
request_id
|
|
76274
|
+
};
|
|
76275
|
+
}
|
|
76276
|
+
if (code === "E_QUOTA_EXCEEDED") {
|
|
76277
|
+
const current = typeof extra.current === "number" ? extra.current : MAX_ENTRIES_PER_AGENT;
|
|
76278
|
+
return {
|
|
76279
|
+
v: 1,
|
|
76280
|
+
code,
|
|
76281
|
+
human: message,
|
|
76282
|
+
fix: {
|
|
76283
|
+
kind: "quota_exceeded",
|
|
76284
|
+
quota: "schedule_entries_per_agent",
|
|
76285
|
+
current,
|
|
76286
|
+
limit: MAX_ENTRIES_PER_AGENT
|
|
76287
|
+
},
|
|
76288
|
+
request_id
|
|
76289
|
+
};
|
|
76290
|
+
}
|
|
76291
|
+
return;
|
|
76292
|
+
}
|
|
76149
76293
|
function emitError(code, message, extra = {}) {
|
|
76150
|
-
|
|
76294
|
+
const error_envelope = buildEnvelopeForCode(code, message, extra);
|
|
76295
|
+
const line = { code, message, ...extra };
|
|
76296
|
+
if (error_envelope)
|
|
76297
|
+
line.error_envelope = error_envelope;
|
|
76298
|
+
process.stderr.write(JSON.stringify(line) + `
|
|
76151
76299
|
`);
|
|
76152
76300
|
}
|
|
76153
76301
|
function exitCodeFor(code) {
|
|
@@ -76211,7 +76359,8 @@ function scheduleAdd(opts) {
|
|
|
76211
76359
|
ok: false,
|
|
76212
76360
|
code: "E_CRON_TOO_FREQUENT",
|
|
76213
76361
|
message: "cron interval is tighter than the minimum (5 minutes)",
|
|
76214
|
-
exit: 9
|
|
76362
|
+
exit: 9,
|
|
76363
|
+
meta: { requested_interval_min: extractCronSmallestGapMin(opts.cronExpr) }
|
|
76215
76364
|
};
|
|
76216
76365
|
}
|
|
76217
76366
|
const rej = filterOverlaySecrets(dry.doc, "overlay");
|
|
@@ -76229,7 +76378,8 @@ function scheduleAdd(opts) {
|
|
|
76229
76378
|
ok: false,
|
|
76230
76379
|
code: "E_QUOTA_EXCEEDED",
|
|
76231
76380
|
message: `agent already has ${existing.length} overlay entries (max ${MAX_ENTRIES_PER_AGENT})`,
|
|
76232
|
-
exit: 9
|
|
76381
|
+
exit: 9,
|
|
76382
|
+
meta: { current: existing.length }
|
|
76233
76383
|
};
|
|
76234
76384
|
}
|
|
76235
76385
|
const hash2 = cronUnitHash(opts.cronExpr, opts.prompt);
|
|
@@ -76428,7 +76578,7 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
76428
76578
|
else if (process.env.SWITCHROOM_AGENT_NAME)
|
|
76429
76579
|
resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
|
|
76430
76580
|
if (!r.ok) {
|
|
76431
|
-
emitError(r.code, r.message);
|
|
76581
|
+
emitError(r.code, r.message, r.meta ?? {});
|
|
76432
76582
|
appendAudit(resolvedAgent, "schedule.add", { cron: opts.cron, prompt: opts.prompt, name: opts.name, code: r.code, would_recreate: false }, r.exit);
|
|
76433
76583
|
process.exit(r.exit);
|
|
76434
76584
|
}
|
|
@@ -76541,7 +76691,7 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
76541
76691
|
else if (process.env.SWITCHROOM_AGENT_NAME)
|
|
76542
76692
|
resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
|
|
76543
76693
|
if (!r.ok) {
|
|
76544
|
-
emitError(r.code, r.message);
|
|
76694
|
+
emitError(r.code, r.message, r.meta ?? {});
|
|
76545
76695
|
appendAudit(resolvedAgent, "schedule.remove", { ...opts, code: r.code }, r.exit);
|
|
76546
76696
|
process.exit(r.exit);
|
|
76547
76697
|
}
|