@humwick/runner 0.0.0-stage → 0.130.1
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/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +42 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentProfile,
|
|
3
|
+
AgentProfileProjectionAsset,
|
|
4
|
+
AgentProfileProjectionEntry,
|
|
5
|
+
AgentProfileProjectionReport,
|
|
6
|
+
AgentProfileProjectionTrust,
|
|
7
|
+
SpawnProvider,
|
|
8
|
+
} from "@humwick/sdk";
|
|
9
|
+
import { isRecord, mcpServerInfo } from "@humwick/sdk";
|
|
10
|
+
import { getManifest } from "@humwick/providers";
|
|
11
|
+
import type { AssembledLaunch } from "./launch-assembly";
|
|
12
|
+
import { projectSourceEnabled, type ResolvedSourceShadow } from "./provisioning";
|
|
13
|
+
import { RELAY_MCP_SERVER_NAME, SHARED_MCP_METAONLY_CONFIG_FILENAME } from "./relay-mcp";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The projection is a report of assembled launch facts. In particular, it is not a second
|
|
17
|
+
* resolver: a source cell is authoritative only for the assets/flags the assembler put on the
|
|
18
|
+
* launch. The profile is used below only for labels and registry provenance, never for a
|
|
19
|
+
* category mode (the old mode fields were declarations, not launch facts).
|
|
20
|
+
*/
|
|
21
|
+
interface AgentProfileProjectionInput {
|
|
22
|
+
assembled: AssembledLaunch;
|
|
23
|
+
generatedAt?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type MatrixSource = "host" | "project" | "agent-relay";
|
|
27
|
+
type MatrixCapability = "instructions" | "mcp" | "hooks" | "skills" | "plugins";
|
|
28
|
+
|
|
29
|
+
const MATRIX_SOURCES: readonly MatrixSource[] = ["host", "project", "agent-relay"];
|
|
30
|
+
const MATRIX_CAPABILITIES: readonly MatrixCapability[] = ["instructions", "mcp", "hooks", "skills", "plugins"];
|
|
31
|
+
|
|
32
|
+
export function agentProfileProjectionReport(input: AgentProfileProjectionInput): AgentProfileProjectionReport {
|
|
33
|
+
const { assembled } = input;
|
|
34
|
+
const entries: AgentProfileProjectionEntry[] = [
|
|
35
|
+
...matrixEntries(assembled),
|
|
36
|
+
relayStatusLineEntry(assembled),
|
|
37
|
+
configHomeEntry(assembled),
|
|
38
|
+
launchShapeEntry(assembled),
|
|
39
|
+
localSettingsEntry(assembled),
|
|
40
|
+
filesystemEntry(assembled.profile),
|
|
41
|
+
envEntry(assembled.profile),
|
|
42
|
+
nativeMemoryEntry(assembled),
|
|
43
|
+
sharedMcpEntry(assembled),
|
|
44
|
+
...provisioningEntries(assembled),
|
|
45
|
+
];
|
|
46
|
+
const warnings = entries
|
|
47
|
+
.filter((entry) => entry.result === "partial" || entry.result === "unenforceable" || entry.result === "not-implemented")
|
|
48
|
+
.map((entry) => `${entry.capability}: ${entry.detail}`);
|
|
49
|
+
const unsupported = entries
|
|
50
|
+
.filter((entry) => entry.result === "unenforceable")
|
|
51
|
+
.map((entry) => entry.capability);
|
|
52
|
+
return {
|
|
53
|
+
profileName: assembled.profile?.name ?? "default",
|
|
54
|
+
provider: assembled.provider,
|
|
55
|
+
base: assembled.base,
|
|
56
|
+
generatedAt: input.generatedAt ?? Date.now(),
|
|
57
|
+
entries,
|
|
58
|
+
warnings,
|
|
59
|
+
unsupported,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function matrixEntries(a: AssembledLaunch): AgentProfileProjectionEntry[] {
|
|
64
|
+
const cells: Record<`${MatrixSource}.${MatrixCapability}`, AgentProfileProjectionEntry> = {
|
|
65
|
+
"host.instructions": hostInstructions(a),
|
|
66
|
+
"host.mcp": hostMcp(a),
|
|
67
|
+
"host.hooks": hostHooks(a),
|
|
68
|
+
"host.skills": hostSkills(a),
|
|
69
|
+
"host.plugins": hostPlugins(a),
|
|
70
|
+
"project.instructions": projectInstructions(a),
|
|
71
|
+
"project.mcp": projectMcp(a),
|
|
72
|
+
"project.hooks": projectHooks(a),
|
|
73
|
+
"project.skills": projectSkills(a),
|
|
74
|
+
"project.plugins": projectPlugins(a),
|
|
75
|
+
"agent-relay.instructions": relayInstructions(a),
|
|
76
|
+
"agent-relay.mcp": relayMcp(a),
|
|
77
|
+
"agent-relay.hooks": relayHooks(a),
|
|
78
|
+
"agent-relay.skills": relaySkills(a),
|
|
79
|
+
"agent-relay.plugins": relayPlugins(a),
|
|
80
|
+
};
|
|
81
|
+
addCrossSourceShadows(cells, a);
|
|
82
|
+
return MATRIX_SOURCES.flatMap((source) => MATRIX_CAPABILITIES.map((capability) => cells[`${source}.${capability}`]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The resolver's additive-union precedence is visible in the report even when two provider
|
|
87
|
+
* surfaces happen to accept the same name: reserved/registry/bundled Relay assets outrank
|
|
88
|
+
* Project, which outranks Host. This is observational only; the launch object remains the
|
|
89
|
+
* authority for what was actually emitted.
|
|
90
|
+
*/
|
|
91
|
+
function addCrossSourceShadows(
|
|
92
|
+
cells: Record<`${MatrixSource}.${MatrixCapability}`, AgentProfileProjectionEntry>,
|
|
93
|
+
a: AssembledLaunch,
|
|
94
|
+
): void {
|
|
95
|
+
for (const shadow of a.sourceShadows ?? []) {
|
|
96
|
+
const entry = cells[`${shadow.source}.${shadow.capability === "hooks" ? "hooks" : shadow.capability}`];
|
|
97
|
+
const provenance = shadow.source === "agent-relay"
|
|
98
|
+
? relayProvenance(a, sourceKind(shadow.capability), shadow.name)
|
|
99
|
+
: shadow.source;
|
|
100
|
+
entry.shadowed = dedupeAssets([...(entry.shadowed ?? []), asset(shadow.name, provenance)]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sourceKind(capability: ResolvedSourceShadow["capability"]): "skill" | "plugin" | "hook" | "mcp" {
|
|
105
|
+
if (capability === "skills") return "skill";
|
|
106
|
+
if (capability === "plugins") return "plugin";
|
|
107
|
+
if (capability === "hooks") return "hook";
|
|
108
|
+
return "mcp";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function dedupeAssets(items: AgentProfileProjectionAsset[]): AgentProfileProjectionAsset[] {
|
|
112
|
+
const seen = new Set<string>();
|
|
113
|
+
return items.filter((item) => {
|
|
114
|
+
const key = `${item.name}\u0000${item.provenance}`;
|
|
115
|
+
if (seen.has(key)) return false;
|
|
116
|
+
seen.add(key);
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hostInstructions(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
122
|
+
if (a.instructions.global === "allow") return scalarCell("host.instructions", "allow", "applied", "allow", "provider default/global instruction cascade", "Host/global provider instructions remain available.");
|
|
123
|
+
if (a.instructions.global === "isolated") return scalarCell("host.instructions", "ignore", "disabled", "off", "isolated provider config home", "The managed provider home excludes host/global instructions.");
|
|
124
|
+
return scalarCell("host.instructions", "ignore", "unenforceable", "unknown", "no provider lever", "Host/global instruction exclusion was requested but no launch lever enforced it.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hostMcp(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
128
|
+
if (!a.configHome) return assetCell("host.mcp", "available", "applied", "host config", "provider host configuration", "Ambient host MCP is available in the provider's normal config home.", [], "untrusted");
|
|
129
|
+
const assets = a.mcp.hostServers.map((name) => asset(name, "host"));
|
|
130
|
+
if (assets.length) return assetCell("host.mcp", "available", "applied", assets, "controlled MCP config injection", `Host MCP servers were re-injected into the managed home (${a.mcp.hostServers.join(", ")}).`, [], "untrusted");
|
|
131
|
+
return assetCell("host.mcp", "available", "disabled", [], "isolated provider config home", "Ambient host MCP is not loaded by this managed provider home.", [], "untrusted");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hostHooks(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
135
|
+
if (!a.configHome) return assetCell("host.hooks", "available", "applied", "ambient", "provider host config", "Ambient host hooks remain available in the provider's normal config home.", [], "untrusted");
|
|
136
|
+
if (!a.hostHooksEnforceable) {
|
|
137
|
+
return assetCell("host.hooks", "available", "unenforceable", "unknown", "provider hook capability", "The installed provider cannot honor host hooks in this isolated config home; H.hooks is unenforceable for this launch.", [], "unknown");
|
|
138
|
+
}
|
|
139
|
+
// A host hook artifact must not make missing Relay hooks look applied. This is a launch-wide
|
|
140
|
+
// fact: callers can suppress the Relay hook set while H.hooks was successfully re-injected.
|
|
141
|
+
if (a.hooksSuppressed) {
|
|
142
|
+
return assetCell("host.hooks", "available", "partial", "suppressed", "hook delivery withheld", `Relay hooks were not delivered: ${a.hooksSuppressed.reason}.`, [], "unknown");
|
|
143
|
+
}
|
|
144
|
+
if (a.hostHooks) {
|
|
145
|
+
return assetCell("host.hooks", "available", "applied", "host", "controlled settings/hooks.json injection", "Host provider hooks were re-injected into the managed provider home.", [], "untrusted");
|
|
146
|
+
}
|
|
147
|
+
return assetCell("host.hooks", "available", "disabled", "off", "isolated provider config home", "Ambient host hooks are excluded from the managed provider home.", [], "untrusted");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hostSkills(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
151
|
+
const assets = a.hostSkills.map((item) => asset(item.name, "host"));
|
|
152
|
+
return assetCell("host.skills", assets.length ? "available" : "off", assets.length ? "applied" : "disabled", assets, "host skill directories copied into managed home", assets.length ? `Host skills materialized (${assets.map((item) => item.name).join(", ")}).` : "No host skill directories reached this launch.", [], "untrusted");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hostPlugins(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
156
|
+
const assets = a.hostPluginDirs.map((dir) => asset(dir, "host"));
|
|
157
|
+
return assetCell("host.plugins", assets.length ? "available" : "off", assets.length ? "applied" : "disabled", assets, "host plugin directories passed to provider", assets.length ? `Host plugins reached the launch (${assets.map((item) => item.name).join(", ")}).` : "No host plugin directories reached this launch.", [], "untrusted");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function projectInstructions(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
161
|
+
if (a.instructions.repo === "allow") return scalarCell("project.instructions", "allow", "applied", "allow", projectInstructionMechanism(a), "Project/repo instructions remain available.");
|
|
162
|
+
// #2086 — the native project scope was dropped to gate ambient hooks, but P.instructions
|
|
163
|
+
// re-injected the repo CLAUDE.md/AGENTS.md through the controlled append-system-prompt channel.
|
|
164
|
+
// Report it as applied+honored (trusted), NOT suppressed, so the launch shape is not misreported.
|
|
165
|
+
if (a.instructions.repo === "reinjected") return scalarCell("project.instructions", "allow", "applied", "reinjected", "controlled append-system-prompt re-injection", "Repo CLAUDE.md/AGENTS.md are re-injected through Relay's controlled channel because the native project scope is dropped to gate ambient hooks.", "trusted");
|
|
166
|
+
if (a.instructions.repo === "suppressed") return scalarCell("project.instructions", "ignore", "disabled", "off", projectInstructionMechanism(a), "Project/repo instructions are excluded by the emitted launch shape.");
|
|
167
|
+
return scalarCell("project.instructions", "ignore", "unenforceable", "unknown", "no provider lever", "Project/repo instruction exclusion was requested but was not enforced by this launch.");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function reviewInfo(a: AssembledLaunch, gate?: { observed?: string; repoKey?: string; reviewMode?: "fingerprint" | "commit" }): AgentProfileProjectionEntry["review"] {
|
|
171
|
+
if (!gate?.observed || !gate.repoKey || !gate.reviewMode || !a.profile?.name) return undefined;
|
|
172
|
+
return { profileName: a.profile.name, repoKey: gate.repoKey, mode: gate.reviewMode, observed: gate.observed };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function projectMcp(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
176
|
+
const projectMcpEnabled = projectSourceEnabled({ agentProfile: a.profile }, "mcp");
|
|
177
|
+
if (projectMcpEnabled && a.profile?.projectGates?.mcp && !a.profile.projectGates.mcp.allowed) {
|
|
178
|
+
return { ...assetCell("project.mcp", "enabled", "partial", "suppressed", "project trust gate", a.profile.projectGates.mcp.detail, [], "unknown"), review: reviewInfo(a, a.profile.projectGates.mcp) };
|
|
179
|
+
}
|
|
180
|
+
const assets = a.mcp.projectServers.map((name) => asset(name, "project"));
|
|
181
|
+
return { ...assetCell("project.mcp", assets.length ? "available" : "off", assets.length ? "applied" : "disabled", assets, "controlled project MCP config injection", assets.length ? `Committed project MCP servers reached the launch (${a.mcp.projectServers.join(", ")}).` : "No committed project MCP server reached this launch.", [], "trusted"), review: reviewInfo(a, a.profile?.projectGates?.mcp) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function projectHooks(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
185
|
+
if (!hasSettingSourceSurface(a)) return assetCell("project.hooks", "available", "not-applicable", "n/a", "provider has no project hook surface", "This provider has no project settings/hook surface.", [], "unknown");
|
|
186
|
+
if (a.projectHooksSuppressed) return { ...assetCell("project.hooks", "enabled", "partial", "suppressed", "project trust gate", a.projectHooksSuppressed.reason, [], "unknown"), review: reviewInfo(a, a.profile?.projectGates?.hooks) };
|
|
187
|
+
if (a.profile?.sources?.project?.hooks?.enabled) {
|
|
188
|
+
const assets = a.projectHooks.map((item) => asset(item.name, "project"));
|
|
189
|
+
return { ...assetCell("project.hooks", assets.length ? "enabled" : "off", assets.length ? "applied" : "disabled", assets, "controlled project hook settings injection", assets.length ? `Committed project hooks reached the launch (${assets.map((item) => item.name).join(", ")}).` : "No committed project hook reached this launch.", [], "trusted"), review: reviewInfo(a, a.profile?.projectGates?.hooks) };
|
|
190
|
+
}
|
|
191
|
+
if (settingSourceLoaded(a, "project")) return assetCell("project.hooks", "available", "applied", "ambient", "Claude --setting-sources project", "The project settings hook source loads ambiently; Relay does not govern its execution.", [], "untrusted");
|
|
192
|
+
return assetCell("project.hooks", "available", "disabled", "off", settingSourceMechanism(a), "The project settings hook source is excluded by the emitted launch shape.", [], "untrusted");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function projectSkills(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
196
|
+
const assets = a.projectSkills.map((item) => asset(item.name, "project"));
|
|
197
|
+
return assetCell("project.skills", assets.length ? "available" : "off", assets.length ? "applied" : "disabled", assets, "committed project skill directories materialized into managed home", assets.length ? `Project skills reached the launch (${assets.map((item) => item.name).join(", ")}).` : "No committed project skill directory reached this launch.", [], "trusted");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function projectPlugins(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
201
|
+
if (!hasSettingSourceSurface(a)) return assetCell("project.plugins", "available", "not-applicable", "n/a", "provider has no project plugin lever", "This provider has no project plugin surface.", [], "unknown");
|
|
202
|
+
return assetCell("project.plugins", "available", "not-implemented", "none", "no provider launch lever", "Project plugin provisioning has no provider launch lever yet.", [], "trusted");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function relayInstructions(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
206
|
+
if (a.relayContextPending || a.launchContextPending) {
|
|
207
|
+
return scalarCell("agent-relay.instructions", "enabled", "applied", "pending first provider turn", "runner provider-delivery injection", "Relay-authored instructions are available and will be prepended to the first provider turn; no provider turn has been emitted at launch.");
|
|
208
|
+
}
|
|
209
|
+
const active = a.relayContext || a.instructions.systemPromptAppend;
|
|
210
|
+
return scalarCell("agent-relay.instructions", active ? "enabled" : "disabled", active ? "applied" : "disabled", active ? "injected" : "off", "runner context/prompt injection", active ? "Relay-authored instructions reached the provider turn." : "No Relay-authored instruction material was emitted.");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function relayMcp(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
214
|
+
const assets = [
|
|
215
|
+
...(a.mcp.relayEndpoint ? [asset(mcpServerInfo(a.mcp.generation ?? "relay").name, "reserved")] : []),
|
|
216
|
+
...a.mcp.servers.map((name) => asset(name, relayProvenance(a, "mcp", name))),
|
|
217
|
+
];
|
|
218
|
+
return assetCell("agent-relay.mcp", assets.length ? "enabled" : "disabled", assets.length ? "applied" : "disabled", assets, "Relay MCP endpoint / controlled MCP config", assets.length ? `Relay MCP assets reached the launch (${assets.map((item) => item.name).join(", ")}).` : "No Relay MCP asset reached this launch.", relayShadows(a, "mcp", assets), "trusted");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function relayHooks(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
222
|
+
if (a.hooksSuppressed?.names.length) return assetCell("agent-relay.hooks", "enabled", "partial", [], "hook delivery withheld", `Relay hooks were resolved but suppressed: ${a.hooksSuppressed.reason}.`, [], "trusted");
|
|
223
|
+
const assets = a.hooks.map((item) => asset(item.name, relayProvenance(a, "hook", item.name)));
|
|
224
|
+
return assetCell("agent-relay.hooks", assets.length ? "enabled" : "disabled", assets.length ? "applied" : "disabled", assets, "provider hook settings/hooks.json", assets.length ? `Relay hooks reached the launch (${assets.map((item) => item.name).join(", ")}).` : "No Relay hook reached this launch.", relayShadows(a, "hook", assets), "trusted");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function relaySkills(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
228
|
+
const assets = [
|
|
229
|
+
...(a.relaySkills ? [asset("agent-relay", "bundled")] : []),
|
|
230
|
+
...a.skills.map((item) => asset(item.name, relayProvenance(a, "skill", item.name))),
|
|
231
|
+
];
|
|
232
|
+
return assetCell("agent-relay.skills", assets.length ? "enabled" : "disabled", assets.length ? "applied" : "disabled", assets, "bundled skills config / Relay plugin", assets.length ? `Relay skill assets reached the launch (${assets.map((item) => item.name).join(", ")}).` : "No Relay skill asset reached this launch.", relayShadows(a, "skill", assets), "trusted");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function relayPlugins(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
236
|
+
const assets = [
|
|
237
|
+
...(a.relayPlugin ? [asset(getManifest(a.provider)?.provisioning?.bundledPlugin?.ref ?? "agent-relay", "bundled")] : []),
|
|
238
|
+
...a.plugins.map((item) => asset(item.name, relayProvenance(a, "plugin", item.name))),
|
|
239
|
+
];
|
|
240
|
+
return assetCell("agent-relay.plugins", assets.length ? "enabled" : "disabled", assets.length ? "applied" : "disabled", assets, "provider plugin-dir launch args", assets.length ? `Relay plugin assets reached the launch (${assets.map((item) => item.name).join(", ")}).` : "No Relay plugin asset reached this launch.", relayShadows(a, "plugin", assets), "trusted");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function relayStatusLineEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
244
|
+
return scalarCell("relay.statusLine", a.statusLine ? "enabled" : "disabled", a.statusLine ? "applied" : "disabled", a.statusLine ? "enabled" : "off", "Claude --settings statusLine", a.statusLine ? "Relay status-line settings reached the launch." : "Relay status-line settings were not emitted.");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function configHomeEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
248
|
+
return scalarCell("provider.configHome", a.configHome ? "isolated" : "host", "applied", a.configHome ? "isolated" : "host", a.configHome ? "CLAUDE_CONFIG_DIR/CODEX_HOME" : "provider default config home", a.configHome ? `The launch uses the managed isolated config home ${a.configHome}.` : "The launch uses the provider's host config home.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function launchShapeEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
252
|
+
// #2367 — argv can contain the full system prompt and inlined config. The old cell copied that
|
|
253
|
+
// string into requested, effective, and detail, turning a ~68 KiB launch into ~203 KiB of static
|
|
254
|
+
// agent meta that every status frame then carried through a read/merge/stringify/write cycle.
|
|
255
|
+
// The projection is diagnostic, not the launch authority: retain its useful shape/size facts and
|
|
256
|
+
// keep the actual argv solely in the assembled launch that the adapter consumes.
|
|
257
|
+
const bytes = new TextEncoder().encode(JSON.stringify(a.args)).byteLength;
|
|
258
|
+
const summary = a.args.length ? `${a.args.length} argv entries (${bytes} bytes)` : "provider defaults (0 argv entries)";
|
|
259
|
+
return scalarCell("provider.launchShape", "assembled", "applied", summary, "provider launch arguments emitted by assembler", `The assembler emitted ${summary}. Argument values stay in the launch artifact, not the agent profile projection.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function localSettingsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
263
|
+
if (!hasSettingSourceSurface(a)) return scalarCell("local.settings", "n/a", "not-applicable", "n/a", "provider has no local settings surface", "This provider has no Claude local settings surface.");
|
|
264
|
+
if (settingSourceLoaded(a, "local")) return scalarCell("local.settings", "ambient", "applied", "ambient", "Claude --setting-sources local", "Local settings load ambiently and are not part of the profile matrix.", "untrusted");
|
|
265
|
+
return scalarCell("local.settings", "off", "disabled", "off", settingSourceMechanism(a), "The local settings source is excluded by the emitted launch shape.", "untrusted");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function filesystemEntry(profile: AgentProfile | undefined): AgentProfileProjectionEntry {
|
|
269
|
+
const requested = profile?.permissions.filesystem ?? "host";
|
|
270
|
+
if (requested === "host") return scalarCell("permissions.filesystem", requested, "applied", "host", "orchestrator cwd containment", "No provider-level filesystem sandbox was requested.");
|
|
271
|
+
return scalarCell("permissions.filesystem", requested, "partial", requested, "orchestrator cwd containment", "Relay constrains the spawn cwd; provider-level filesystem projection is incomplete.");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function envEntry(profile: AgentProfile | undefined): AgentProfileProjectionEntry {
|
|
275
|
+
const count = Object.keys(profile?.env ?? {}).length;
|
|
276
|
+
return scalarCell("env", count ? `${count} vars` : "none", "applied", count ? `${count} vars` : "none", "runner environment", count ? "Profile environment variables reached the runner." : "No profile environment variables were requested.");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function nativeMemoryEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
280
|
+
const lever = a.nativeMemory;
|
|
281
|
+
if (lever.suppressed) {
|
|
282
|
+
const effective = lever.applied.length ? lever.applied.join(", ") : "provider default (on)";
|
|
283
|
+
return scalarCell("nativeMemory", lever.policy, "partial", effective, "native-memory launch lever withheld", `Native-memory policy ${lever.policy} was only partially delivered; suppressed ${lever.suppressed.levers.join(", ")}: ${lever.suppressed.reason}.`);
|
|
284
|
+
}
|
|
285
|
+
if (lever.policy === "default") return scalarCell("nativeMemory", "absent", "not-applicable", "provider default", "no native-memory flag", `No native-memory policy was emitted; ${a.provider}'s native memory keeps its default.`);
|
|
286
|
+
const effective = lever.applied.length ? lever.applied.join(", ") : "provider default (on)";
|
|
287
|
+
return scalarCell("nativeMemory", lever.policy, "applied", effective, "native-memory launch lever", `Native-memory policy ${lever.policy} reached the launch (${effective}).`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function sharedMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
291
|
+
const listener = a.mcp.sharedListener;
|
|
292
|
+
if (!listener) return scalarCell("sharedMcp", "off", "not-applicable", "off", "no shared listener", "Shared host-listener MCP is off for this launch.");
|
|
293
|
+
const route = listener.metaOnly
|
|
294
|
+
? `callmux --config <home>/${SHARED_MCP_METAONLY_CONFIG_FILENAME}`
|
|
295
|
+
: `callmux bridge --cwd "$WORKTREE"`;
|
|
296
|
+
const relayNote = listener.relayFronted ? " Relay's identity endpoint is also fronted on-demand through the runner proxy." : " Relay's identity endpoint stays per-agent.";
|
|
297
|
+
return scalarCell("sharedMcp", "on", "applied", "on", route, `Shared MCP routes through ${listener.url} via ${route}.${relayNote}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function provisioningEntries(a: AssembledLaunch): AgentProfileProjectionEntry[] {
|
|
301
|
+
const entries: AgentProfileProjectionEntry[] = [];
|
|
302
|
+
for (const item of a.skills) entries.push(assetCell(`provisioning.skill.${item.name}`, "enabled", "applied", [asset(item.name, relayProvenance(a, "skill", item.name))], "provider skill launch", `Provisioned skill ${item.name} reached the launch.`));
|
|
303
|
+
for (const item of a.plugins) entries.push(assetCell(`provisioning.plugin.${item.name}`, "enabled", "applied", [asset(item.name, relayProvenance(a, "plugin", item.name))], "provider plugin-dir launch", `Provisioned plugin ${item.name} reached the launch.`));
|
|
304
|
+
for (const item of a.hooks) entries.push(assetCell(`provisioning.hook.${item.name}`, "enabled", "applied", [asset(item.name, relayProvenance(a, "hook", item.name))], "provider hook settings/hooks.json", `Provisioned hook ${item.name} reached the launch.`));
|
|
305
|
+
for (const name of a.mcp.servers) entries.push(assetCell(`provisioning.mcp.${name}`, "enabled", "applied", [asset(name, relayProvenance(a, "mcp", name))], "controlled MCP config", `Provisioned MCP server ${name} reached the launch.`));
|
|
306
|
+
for (const item of a.profile?.provisioning?.skipped ?? []) entries.push(scalarCell(`provisioning.${item.kind}.${item.ref}`, item.reason, "partial", "suppressed", "provider variant resolution", item.detail));
|
|
307
|
+
return entries;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function scalarCell(capability: string, requested: string, result: AgentProfileProjectionEntry["result"], effective: string, mechanism: string, detail: string, trust?: AgentProfileProjectionTrust): AgentProfileProjectionEntry {
|
|
311
|
+
return { capability, requested, result, effective, mechanism, ...(trust ? { trust } : {}), detail };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function assetCell(capability: string, requested: string, result: AgentProfileProjectionEntry["result"], effective: string | AgentProfileProjectionAsset[], mechanism: string, detail: string, shadowed: AgentProfileProjectionAsset[] = [], trust?: AgentProfileProjectionTrust): AgentProfileProjectionEntry {
|
|
315
|
+
return { capability, requested, result, effective, mechanism, ...(shadowed.length ? { shadowed } : {}), ...(trust ? { trust } : {}), detail };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function asset(name: string, provenance: string): AgentProfileProjectionAsset {
|
|
319
|
+
return { name, provenance };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function hasSettingSourceSurface(a: AssembledLaunch): boolean {
|
|
323
|
+
return Boolean(getManifest(a.provider)?.provisioning?.bundledPlugin);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function settingSourceLoaded(a: AssembledLaunch, scope: string): boolean {
|
|
327
|
+
return !a.settingSources || a.settingSources.includes(scope);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function settingSourceMechanism(a: AssembledLaunch): string {
|
|
331
|
+
return `Claude --setting-sources ${(a.settingSources ?? ["user", "project", "local"]).join(",")}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function projectInstructionMechanism(a: AssembledLaunch): string {
|
|
335
|
+
if (a.settingSources) return settingSourceMechanism(a);
|
|
336
|
+
return a.vanilla ? "provider vanilla project-doc suppression" : "provider default project instruction cascade";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
type ProvenanceTier = "reserved" | "registry" | "bundled";
|
|
340
|
+
interface RelaySourceKindTiers { reserved: string[]; registry: string[]; bundled: string[] }
|
|
341
|
+
|
|
342
|
+
function relayRegistryRefs(refs: AgentProfile["skills"] | undefined): string[] {
|
|
343
|
+
return (refs ?? []).filter((ref) => ref.source === "relay" && ref.enabled).map((ref) => ref.ref);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function relaySourceKindTiers(a: AssembledLaunch): Record<"skill" | "plugin" | "hook" | "mcp", RelaySourceKindTiers> {
|
|
347
|
+
const profile = a.profile;
|
|
348
|
+
const provisioning = getManifest(a.provider)?.provisioning;
|
|
349
|
+
const bundledHooks = provisioning?.bundledHooks ?? [];
|
|
350
|
+
const skippedMcpRefs = new Set(
|
|
351
|
+
(profile?.provisioning?.skipped ?? [])
|
|
352
|
+
.filter((item) => item.kind === "mcp")
|
|
353
|
+
.map((item) => item.ref),
|
|
354
|
+
);
|
|
355
|
+
const mcpRegistry = Object.values(profile?.mcp.servers ?? {})
|
|
356
|
+
.filter((value): value is { source: string; ref: string } => isRecord(value) && value.source === "relay" && typeof value.ref === "string" && !skippedMcpRefs.has(value.ref))
|
|
357
|
+
.map((value) => value.ref);
|
|
358
|
+
return {
|
|
359
|
+
skill: { reserved: [], registry: relayRegistryRefs(profile?.skills), bundled: a.relaySkills ? ["agent-relay"] : [] },
|
|
360
|
+
plugin: { reserved: [], registry: relayRegistryRefs(profile?.plugins), bundled: a.relayPlugin && provisioning?.bundledPlugin ? [provisioning.bundledPlugin.ref] : [] },
|
|
361
|
+
hook: { reserved: bundledHooks.filter((hook) => hook.baseline).map((hook) => hook.ref), registry: relayRegistryRefs(profile?.hooks?.refs), bundled: bundledHooks.filter((hook) => !hook.baseline).map((hook) => hook.ref) },
|
|
362
|
+
mcp: { reserved: a.mcp.relayEndpoint ? [RELAY_MCP_SERVER_NAME] : [], registry: mcpRegistry, bundled: [] },
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function relayProvenance(a: AssembledLaunch, kind: "skill" | "plugin" | "hook" | "mcp", name: string): string {
|
|
367
|
+
const tiers = relaySourceKindTiers(a)[kind];
|
|
368
|
+
if (tiers.reserved.includes(name)) return "reserved";
|
|
369
|
+
const ref = profileAssetRef(a, kind, name);
|
|
370
|
+
// MCP registry entries are keyed by the profile-facing server name, while the
|
|
371
|
+
// registry tier is keyed by the canonical ref. Preserve that ref when the
|
|
372
|
+
// effective server uses an alias.
|
|
373
|
+
if (ref?.source === "relay") {
|
|
374
|
+
return tiers.registry.includes(ref.ref) ? `registry:${ref.ref}` : "unknown";
|
|
375
|
+
}
|
|
376
|
+
if (tiers.registry.includes(name)) return `registry:${name}`;
|
|
377
|
+
if (tiers.bundled.includes(name)) return "bundled";
|
|
378
|
+
if (ref?.source && ref.source !== "relay") return ref.source;
|
|
379
|
+
return "unknown";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function profileAssetRef(a: AssembledLaunch, kind: "skill" | "plugin" | "hook" | "mcp", name: string): { source: string; ref: string } | undefined {
|
|
383
|
+
if (kind === "mcp") {
|
|
384
|
+
const value = a.profile?.mcp.servers?.[name];
|
|
385
|
+
return isRecord(value) && typeof value.source === "string" && typeof value.ref === "string"
|
|
386
|
+
? { source: value.source, ref: value.ref }
|
|
387
|
+
: undefined;
|
|
388
|
+
}
|
|
389
|
+
const refs = kind === "skill"
|
|
390
|
+
? a.profile?.skills
|
|
391
|
+
: kind === "plugin"
|
|
392
|
+
? a.profile?.plugins
|
|
393
|
+
: a.profile?.hooks.refs;
|
|
394
|
+
const ref = refs?.find((candidate) => candidate.enabled && candidate.ref === name);
|
|
395
|
+
return ref ? { source: ref.source, ref: ref.ref } : undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function relayShadows(a: AssembledLaunch, kind: "skill" | "plugin" | "hook" | "mcp", effective: AgentProfileProjectionAsset[]): AgentProfileProjectionAsset[] {
|
|
399
|
+
const tiers = relaySourceKindTiers(a)[kind];
|
|
400
|
+
const rank: Record<ProvenanceTier, number> = { reserved: 0, registry: 1, bundled: 2 };
|
|
401
|
+
const winner = new Map<string, ProvenanceTier>();
|
|
402
|
+
for (const tier of ["reserved", "registry", "bundled"] as const) for (const name of tiers[tier]) {
|
|
403
|
+
const existing = winner.get(name);
|
|
404
|
+
if (existing === undefined || rank[tier] < rank[existing]) winner.set(name, tier);
|
|
405
|
+
}
|
|
406
|
+
const out: AgentProfileProjectionAsset[] = [];
|
|
407
|
+
for (const tier of ["reserved", "registry", "bundled"] as const) for (const name of tiers[tier]) {
|
|
408
|
+
if (winner.get(name) !== tier && effective.some((item) => item.name === name)) out.push(asset(name, tier === "registry" ? `registry:${name}` : tier));
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ProvisioningHookCommand, ProvisioningHookMatcher, ProvisioningHookSet } from "@humwick/sdk";
|
|
4
|
+
import type { RunnerSpawnConfig } from "./adapter";
|
|
5
|
+
import type { ResolvedHookAsset } from "./provisioning";
|
|
6
|
+
|
|
7
|
+
function validCommand(value: unknown): value is ProvisioningHookCommand {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
9
|
+
const command = (value as Record<string, unknown>).command;
|
|
10
|
+
return typeof command === "string" && command.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseHookSet(value: unknown): ProvisioningHookSet {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
15
|
+
const out: ProvisioningHookSet = {};
|
|
16
|
+
for (const [event, rawMatchers] of Object.entries(value as Record<string, unknown>)) {
|
|
17
|
+
if (!Array.isArray(rawMatchers)) continue;
|
|
18
|
+
const matchers: ProvisioningHookMatcher[] = [];
|
|
19
|
+
for (const rawMatcher of rawMatchers) {
|
|
20
|
+
if (!rawMatcher || typeof rawMatcher !== "object" || Array.isArray(rawMatcher)) continue;
|
|
21
|
+
const matcher = rawMatcher as Record<string, unknown>;
|
|
22
|
+
if (!Array.isArray(matcher.hooks)) continue;
|
|
23
|
+
const commands = matcher.hooks.filter(validCommand).map((command) => ({
|
|
24
|
+
...command,
|
|
25
|
+
...(typeof command.timeout === "number" ? { timeout: command.timeout } : {}),
|
|
26
|
+
...(typeof command.statusMessage === "string" ? { statusMessage: command.statusMessage } : {}),
|
|
27
|
+
...(typeof command.suppressOutput === "boolean" ? { suppressOutput: command.suppressOutput } : {}),
|
|
28
|
+
}));
|
|
29
|
+
if (!commands.length) continue;
|
|
30
|
+
matchers.push({
|
|
31
|
+
...(typeof matcher.matcher === "string" ? { matcher: matcher.matcher } : {}),
|
|
32
|
+
hooks: commands,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (matchers.length) out[event] = matchers;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolve the committed project `.claude/settings.json` hook surface. The
|
|
41
|
+
* source is tagged as `project` so reports/materializers can distinguish it from
|
|
42
|
+
* Relay-owned hooks. Trust decisions are server-stamped on the in-flight profile;
|
|
43
|
+
* missing decisions fail closed. */
|
|
44
|
+
export function resolveProjectHooks(config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">): ResolvedHookAsset[] {
|
|
45
|
+
const profile = config.agentProfile;
|
|
46
|
+
if (!profile?.sources?.project?.hooks?.enabled) return [];
|
|
47
|
+
if (!profile.projectGates?.hooks?.allowed) return [];
|
|
48
|
+
const file = join(config.cwd, ".claude", "settings.json");
|
|
49
|
+
if (!existsSync(file)) return [];
|
|
50
|
+
let parsed: unknown;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const hooks = parseHookSet((parsed as Record<string, unknown> | null)?.hooks);
|
|
57
|
+
if (!Object.keys(hooks).length) return [];
|
|
58
|
+
return [{
|
|
59
|
+
name: "project-hooks",
|
|
60
|
+
dir: join(config.cwd, ".claude"),
|
|
61
|
+
source: { kind: "inPlace" },
|
|
62
|
+
provenance: "project",
|
|
63
|
+
hooks,
|
|
64
|
+
}];
|
|
65
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { sanitizeLaunchText } from "@humwick/sdk";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { RunnerSpawnConfig } from "./adapter";
|
|
5
|
+
import { projectSettingSourcesOn } from "./profile-home";
|
|
6
|
+
|
|
7
|
+
// The repo instruction docs Claude's native project cascade loads from the spawn cwd
|
|
8
|
+
// (walking is Claude's job; the profile matrix governs only whether the scope loads).
|
|
9
|
+
// Codex's sibling is AGENTS.md via project_doc — but Codex has no P.hooks→scope-drop
|
|
10
|
+
// interference (only a vanilla base suppresses its project doc), so this resolver is
|
|
11
|
+
// the Claude re-injection channel and lists both names the cwd may carry.
|
|
12
|
+
const REPO_INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"] as const;
|
|
13
|
+
|
|
14
|
+
// Bound each re-injected file so a pathological repo doc can't balloon the launch argv
|
|
15
|
+
// (append-system-prompt rides the command line). Mirrors the spirit of Codex's
|
|
16
|
+
// project_doc_max_bytes default; oversized content is truncated with a loud marker
|
|
17
|
+
// rather than silently dropped.
|
|
18
|
+
const REPO_INSTRUCTIONS_MAX_BYTES = 64 * 1024;
|
|
19
|
+
|
|
20
|
+
export interface ResolvedProjectInstructions {
|
|
21
|
+
/** The composed append-system-prompt block, ready to fold into the launch. */
|
|
22
|
+
text: string;
|
|
23
|
+
/** The repo instruction filenames that actually contributed (for the report). */
|
|
24
|
+
files: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ComposedSystemPromptAppend {
|
|
28
|
+
/** The caller's systemPromptAppend and any re-injected repo instructions, joined. */
|
|
29
|
+
text: string;
|
|
30
|
+
/** Whether repo instructions were re-injected (drives the launch's repo disposition). */
|
|
31
|
+
reinjected: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fold the caller's `systemPromptAppend` together with any #2086 repo re-injection into the single
|
|
36
|
+
* `--append-system-prompt` value the launch emits. The two channels are distinct in reporting
|
|
37
|
+
* (relay-authored append vs. repo-doc re-injection), so `reinjected` is returned separately.
|
|
38
|
+
*/
|
|
39
|
+
export function composeSystemPromptAppend(config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd" | "systemPromptAppend">): ComposedSystemPromptAppend {
|
|
40
|
+
const reinjected = resolveProjectInstructions(config);
|
|
41
|
+
const text = [config.systemPromptAppend && sanitizeLaunchText(config.systemPromptAppend, "systemPromptAppend"), reinjected?.text]
|
|
42
|
+
.filter((part): part is string => Boolean(part && part.trim()))
|
|
43
|
+
.join("\n\n");
|
|
44
|
+
return { text, reinjected: Boolean(reinjected) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* #2086 — the instructions sibling of {@link resolveProjectHooks}. When `P.hooks` is
|
|
49
|
+
* named, the runner drops the ENTIRE native project setting-source scope so ambient repo
|
|
50
|
+
* hooks can never bypass the trust gate (profile-home `projectSettingSourcesOn`). That drop
|
|
51
|
+
* also strips the repo CLAUDE.md/AGENTS.md cascade even when `P.instructions` is on, silently
|
|
52
|
+
* defeating it. This resolver re-injects those repo docs through Relay's controlled channel
|
|
53
|
+
* (append-system-prompt) so `P.instructions=on` is honored regardless of `P.hooks`.
|
|
54
|
+
*
|
|
55
|
+
* Unlike hooks (executing surfaces gated by `projectGates`), repo instructions are inert
|
|
56
|
+
* text that already loaded ambiently whenever the project scope was present — so re-injection
|
|
57
|
+
* carries no new trust decision and needs no gate. It fires ONLY when the scope is dropped, so
|
|
58
|
+
* a launch that still loads the project scope natively never double-injects.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveProjectInstructions(config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">): ResolvedProjectInstructions | undefined {
|
|
61
|
+
if (!projectInstructionsReinjected(config)) return undefined;
|
|
62
|
+
if (!config.cwd) return undefined;
|
|
63
|
+
const blocks: string[] = [];
|
|
64
|
+
const files: string[] = [];
|
|
65
|
+
// Dedupe by realpath (AGENTS.md -> CLAUDE.md) and by content (a plain copy).
|
|
66
|
+
const seenReal = new Map<string, string>();
|
|
67
|
+
const seenContent = new Map<string, string>();
|
|
68
|
+
for (const name of REPO_INSTRUCTION_FILES) {
|
|
69
|
+
const file = join(config.cwd, name);
|
|
70
|
+
if (!existsSync(file)) continue;
|
|
71
|
+
let content: string;
|
|
72
|
+
try {
|
|
73
|
+
const real = realpathSync(file);
|
|
74
|
+
const sameFile = seenReal.get(real);
|
|
75
|
+
if (sameFile) {
|
|
76
|
+
files.push(`${name} (same as ${sameFile})`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seenReal.set(real, name);
|
|
80
|
+
content = readFileSync(file, "utf8");
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!content.trim()) continue;
|
|
85
|
+
const sameContent = seenContent.get(content);
|
|
86
|
+
if (sameContent) {
|
|
87
|
+
files.push(`${name} (same as ${sameContent})`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
seenContent.set(content, name);
|
|
91
|
+
blocks.push(renderRepoInstructionBlock(name, sanitizeLaunchText(content, name)));
|
|
92
|
+
files.push(name);
|
|
93
|
+
}
|
|
94
|
+
if (!blocks.length) return undefined;
|
|
95
|
+
return { text: `${REPO_INSTRUCTIONS_PREAMBLE}\n\n${blocks.join("\n\n")}`, files };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether this launch must re-inject the repo instruction docs — i.e. `P.instructions` is
|
|
100
|
+
* enabled but the native project scope is dropped (today only `P.hooks=on` forces that). Kept
|
|
101
|
+
* general (`!projectSettingSourcesOn`) so it stays correct if the scope-drop set ever widens.
|
|
102
|
+
*/
|
|
103
|
+
export function projectInstructionsReinjected(config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">): boolean {
|
|
104
|
+
if (!config.agentProfile?.sources?.project?.instructions?.enabled) return false;
|
|
105
|
+
return !projectSettingSourcesOn(config);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const REPO_INSTRUCTIONS_PREAMBLE =
|
|
109
|
+
"[agent-relay] The repository instruction docs below are re-injected by Relay because the " +
|
|
110
|
+
"native project setting-source scope was dropped to gate ambient project hooks. Follow them " +
|
|
111
|
+
"as the repository's CLAUDE.md/AGENTS.md guidance.";
|
|
112
|
+
|
|
113
|
+
function renderRepoInstructionBlock(name: string, content: string): string {
|
|
114
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
115
|
+
const body = bytes > REPO_INSTRUCTIONS_MAX_BYTES
|
|
116
|
+
? `${Buffer.from(content, "utf8").subarray(0, REPO_INSTRUCTIONS_MAX_BYTES).toString("utf8")}\n[agent-relay: ${name} truncated at ${REPO_INSTRUCTIONS_MAX_BYTES} bytes]`
|
|
117
|
+
: content.replace(/\s+$/, "");
|
|
118
|
+
return `<repo-instructions source="${name}">\n${body}\n</repo-instructions>`;
|
|
119
|
+
}
|