@gethmy/harness 1.0.0
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/README.md +66 -0
- package/dist/cli.js +2936 -0
- package/dist/index.js +3734 -0
- package/package.json +65 -0
- package/src/artifact-judge.ts +410 -0
- package/src/cli.ts +272 -0
- package/src/command-metric.ts +594 -0
- package/src/error-classifier.ts +95 -0
- package/src/exec-types.ts +109 -0
- package/src/gate-collectors.ts +431 -0
- package/src/gate-config-error.ts +73 -0
- package/src/git-diff-stat.ts +148 -0
- package/src/git-pr.ts +839 -0
- package/src/harmony-client.ts +197 -0
- package/src/index.ts +37 -0
- package/src/log.ts +129 -0
- package/src/model-tier.test.ts +169 -0
- package/src/model-tier.ts +108 -0
- package/src/oracle-collector.ts +148 -0
- package/src/oracle.ts +434 -0
- package/src/pm.ts +73 -0
- package/src/process-group.ts +149 -0
- package/src/project-type.ts +303 -0
- package/src/revert-guard.ts +99 -0
- package/src/review-types.ts +52 -0
- package/src/runner.ts +184 -0
- package/src/sdk-agent-runner.ts +575 -0
- package/src/stage-cli.ts +302 -0
- package/src/stage-run.ts +91 -0
- package/src/verification.ts +711 -0
- package/src/worktree.ts +639 -0
package/src/runner.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner module: build the launch for one stage's subagent. One interface, one
|
|
3
|
+
* implementation — seam ready, no registry.
|
|
4
|
+
*
|
|
5
|
+
* `cli.ts` is the production caller (Task 15): it builds the launch, hands
|
|
6
|
+
* `disallowedTools` to `SdkAgentRunner`, and turns this launch's env omissions
|
|
7
|
+
* into the DELETE the spawned child needs via {@link envKeysDroppedByLaunch}
|
|
8
|
+
* (an omission alone strips nothing — `spawnInGroup` rebuilds the child's
|
|
9
|
+
* environment from the motor's own `process.env`).
|
|
10
|
+
*
|
|
11
|
+
* An `implementer` subagent's launch carries two measures against the held
|
|
12
|
+
* oracle:
|
|
13
|
+
*
|
|
14
|
+
* 1. Every Harmony credential is stripped from its environment (below).
|
|
15
|
+
* 2. Its `disallowedTools` carries a scoped `Read` deny over the credential
|
|
16
|
+
* directory, `~/.harmony-mcp`. That path is resolved via `getConfigDir()`,
|
|
17
|
+
* imported from `@gethmy/mcp/src/config.js` (the same import specifier
|
|
18
|
+
* `packages/harmony-agent/src/config.ts` already uses); the backing
|
|
19
|
+
* implementation, `packages/mcp-server/src/config.ts`, is the source of
|
|
20
|
+
* truth for the path and for `getActiveCredential()`, which prefers
|
|
21
|
+
* `oauthAccessToken` then `apiKey`.
|
|
22
|
+
*
|
|
23
|
+
* Neither measure closes the hole, and this module does not claim it does.
|
|
24
|
+
* `cli.ts` passes no `allowedTools`, so `SDK_ALLOWED_TOOLS`'s defaults
|
|
25
|
+
* (sdk-agent-runner.ts) apply and the implementer keeps a wide retained
|
|
26
|
+
* surface:
|
|
27
|
+
*
|
|
28
|
+
* - `Bash` stays allowed — the stage still needs it to build and run tests —
|
|
29
|
+
* and the credential file is an ordinary readable file, so
|
|
30
|
+
* `cat ~/.harmony-mcp/config.json` still works from inside a Bash call.
|
|
31
|
+
* - `Grep` and `Glob` stay allowed too, and neither is covered by the deny
|
|
32
|
+
* above (which names only `Read`): both can read the credential file's
|
|
33
|
+
* content or prove its existence.
|
|
34
|
+
* - `mcp__harmony__*` stays allowed. The harmony MCP server resolves its own
|
|
35
|
+
* credential straight from the file — `getActiveCredential()` calls
|
|
36
|
+
* `loadConfig()` with no environment fallback
|
|
37
|
+
* (packages/mcp-server/src/config.ts) — so neither the env strip nor the
|
|
38
|
+
* Read deny can disarm it: a `Read`-tool deny governs the agent's own Read
|
|
39
|
+
* tool, not a separate process's filesystem access. No MCP tool serves an
|
|
40
|
+
* oracle today, which is the only reason this is not yet a live path to
|
|
41
|
+
* one.
|
|
42
|
+
* - `env` still carries `HOME`. It has to: that is what makes
|
|
43
|
+
* `~/.harmony-mcp` locatable at all, for `getConfigDir()` and for the
|
|
44
|
+
* subagent's own shell alike.
|
|
45
|
+
*
|
|
46
|
+
* So "only `author`/`reviewer` may hold Harmony credentials" (below) describes
|
|
47
|
+
* the env strip's one target — the HTTP API credential in the environment —
|
|
48
|
+
* and nothing wider: an implementer still reaches Harmony through
|
|
49
|
+
* `mcp__harmony__*`, authenticated by the MCP server's own file-based
|
|
50
|
+
* credential, untouched by anything here. The env strip and the Read deny
|
|
51
|
+
* raise the bar against a stray env-var read or a direct Read-tool call;
|
|
52
|
+
* neither is a sandbox boundary, and neither covers Grep, Glob, or the MCP
|
|
53
|
+
* surface.
|
|
54
|
+
*
|
|
55
|
+
* The real locks on an oracle READ live in oracle-core.ts, not here:
|
|
56
|
+
* `purpose === "gate_evaluation"`, the target stage declaring an
|
|
57
|
+
* `oracle_passed` gate, and — enforced by the route before either function
|
|
58
|
+
* runs — binding the caller to the card's active agent session. There is
|
|
59
|
+
* deliberately no role check on read (ruling 27): the stage whose gate reads
|
|
60
|
+
* the oracle back IS the implementer stage, so a role check there would
|
|
61
|
+
* refuse every legitimate read.
|
|
62
|
+
*
|
|
63
|
+
* The launch also tells the implementer nothing it does not need: no session
|
|
64
|
+
* id, no oracle path. `RoleLaunch` below has no field for either.
|
|
65
|
+
*
|
|
66
|
+
* Fail closed: an absent role, or one that does not normalize
|
|
67
|
+
* (`normalizeStageRole`), is treated like `implementer`.
|
|
68
|
+
*/
|
|
69
|
+
import { getConfigDir } from "@gethmy/mcp/src/config.js";
|
|
70
|
+
import { normalizeStageRole, type PlaybookStageRole } from "@harmony/shared";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Environment keys that carry Harmony access. Add every new one here — a key that
|
|
74
|
+
* is not listed is a key that leaks.
|
|
75
|
+
*/
|
|
76
|
+
export const HARMONY_CREDENTIAL_KEYS: readonly string[] = [
|
|
77
|
+
"HARMONY_API_KEY",
|
|
78
|
+
"HARMONY_API_URL",
|
|
79
|
+
"HARMONY_WORKSPACE_ID",
|
|
80
|
+
"SUPABASE_ANON_KEY",
|
|
81
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
82
|
+
"SUPABASE_URL",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export interface RoleLaunch {
|
|
86
|
+
/**
|
|
87
|
+
* The normalized role (`normalizeStageRole` — see `buildRoleLaunch`), never
|
|
88
|
+
* the raw input. A forged or malformed role never reaches a consumer of this
|
|
89
|
+
* field, mirroring `getStageRole`'s contract in playbookStage.ts.
|
|
90
|
+
*/
|
|
91
|
+
role: PlaybookStageRole | null;
|
|
92
|
+
prompt: string;
|
|
93
|
+
repoPath: string;
|
|
94
|
+
env: Record<string, string>;
|
|
95
|
+
/**
|
|
96
|
+
* `disallowedTools` entries in the SDK's array form (see sdk-agent-runner.ts:
|
|
97
|
+
* a disallow wins over an allow). Non-empty exactly when the role may not
|
|
98
|
+
* hold Harmony credentials, in which case it carries one scoped `Read` deny
|
|
99
|
+
* over the credential directory. Empty for `author`/`reviewer`, who read the
|
|
100
|
+
* credential legitimately. `cli.ts` hands this to `SdkAgentRunner`. See the
|
|
101
|
+
* module doc comment above for what this narrows and — just as important —
|
|
102
|
+
* what it does not: `Grep`, `Glob`, and `mcp__harmony__*` all stay reachable
|
|
103
|
+
* regardless of this field.
|
|
104
|
+
*/
|
|
105
|
+
disallowedTools: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Only `author` and `reviewer` may hold the Harmony HTTP-API credential in
|
|
110
|
+
* their launch environment. This governs the env strip only — it says nothing
|
|
111
|
+
* about `mcp__harmony__*` access, which every role keeps; see the module doc
|
|
112
|
+
* comment for what that means in practice for the implementer.
|
|
113
|
+
*/
|
|
114
|
+
function mayHoldCredentials(role: PlaybookStageRole | null): boolean {
|
|
115
|
+
return role === "author" || role === "reviewer";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A scoped `Read` deny over the credential directory. A `//`-prefixed pattern
|
|
120
|
+
* anchors to an absolute filesystem path; a single leading slash would instead
|
|
121
|
+
* anchor to the launch's `repoPath` (the Agent SDK's permission-rule scoping for
|
|
122
|
+
* `disallowedTools`), which is not what we want since `~/.harmony-mcp` sits
|
|
123
|
+
* outside the worktree.
|
|
124
|
+
*/
|
|
125
|
+
function credentialReadDeny(): string {
|
|
126
|
+
return `Read(/${getConfigDir()}/**)`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildRoleLaunch(args: {
|
|
130
|
+
role: PlaybookStageRole | null;
|
|
131
|
+
prompt: string;
|
|
132
|
+
repoPath: string;
|
|
133
|
+
parentEnv: Record<string, string | undefined>;
|
|
134
|
+
}): RoleLaunch {
|
|
135
|
+
// Defense in depth, not a replacement for the caller's own validation: a
|
|
136
|
+
// stage's `role` ultimately comes off opaque JSONB (`playbooks.steps`) with
|
|
137
|
+
// no server-side schema validation, so the `PlaybookStageRole | null` type
|
|
138
|
+
// above is a claim the caller makes, not a guarantee this function can trust.
|
|
139
|
+
// Re-normalizing here, the same way `getStageRole` does at the read boundary
|
|
140
|
+
// in playbookStage.ts, means a forged or mis-cased value (e.g. "Implementer"
|
|
141
|
+
// — the exact shape ruling 27 had to fix in oracle-core.ts) fails closed
|
|
142
|
+
// here too, independent of whether the caller already normalized it.
|
|
143
|
+
const role = normalizeStageRole(args.role);
|
|
144
|
+
const keep = mayHoldCredentials(role);
|
|
145
|
+
const env: Record<string, string> = {};
|
|
146
|
+
|
|
147
|
+
for (const [key, value] of Object.entries(args.parentEnv)) {
|
|
148
|
+
if (value === undefined) continue;
|
|
149
|
+
if (!keep && HARMONY_CREDENTIAL_KEYS.includes(key)) continue;
|
|
150
|
+
env[key] = value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
role,
|
|
155
|
+
prompt: args.prompt,
|
|
156
|
+
repoPath: args.repoPath,
|
|
157
|
+
env,
|
|
158
|
+
disallowedTools: keep ? [] : [credentialReadDeny()],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The keys `buildRoleLaunch` DROPPED from `parentEnv` — the strip set
|
|
164
|
+
* `SdkRunnerConfig.stripEnvKeys` (sdk-agent-runner.ts) hands to `spawnInGroup`.
|
|
165
|
+
*
|
|
166
|
+
* Derived from the launch rather than re-listing the credential keys, so
|
|
167
|
+
* `buildRoleLaunch` above stays the single source of truth: a key added to
|
|
168
|
+
* {@link HARMONY_CREDENTIAL_KEYS} is stripped from the child process without a
|
|
169
|
+
* second list anywhere. A key whose parent value is `undefined` is not
|
|
170
|
+
* reported — it reaches no child either way.
|
|
171
|
+
*
|
|
172
|
+
* This function reports what the launch omitted; it does not itself remove
|
|
173
|
+
* anything. The deletion happens in `spawnInGroup`, and only a caller that
|
|
174
|
+
* passes this set to a runner gets it. See that function's doc comment for why
|
|
175
|
+
* a merge cannot substitute for the delete.
|
|
176
|
+
*/
|
|
177
|
+
export function envKeysDroppedByLaunch(
|
|
178
|
+
parentEnv: Record<string, string | undefined>,
|
|
179
|
+
launch: Pick<RoleLaunch, "env">,
|
|
180
|
+
): string[] {
|
|
181
|
+
return Object.keys(parentEnv).filter(
|
|
182
|
+
(key) => parentEnv[key] !== undefined && !Object.hasOwn(launch.env, key),
|
|
183
|
+
);
|
|
184
|
+
}
|