@onepointfour-packs/flaredeck-agent 0.1.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/LICENSE.md +14 -0
- package/README.md +102 -0
- package/dist/agent.d.ts +104 -0
- package/dist/agent.js +329 -0
- package/dist/agent.js.map +1 -0
- package/dist/approval.d.ts +72 -0
- package/dist/approval.js +161 -0
- package/dist/approval.js.map +1 -0
- package/dist/approvals.d.ts +2 -0
- package/dist/approvals.js +148 -0
- package/dist/approvals.js.map +1 -0
- package/dist/auth.d.ts +79 -0
- package/dist/auth.js +181 -0
- package/dist/auth.js.map +1 -0
- package/dist/automode.d.ts +33 -0
- package/dist/automode.js +69 -0
- package/dist/automode.js.map +1 -0
- package/dist/branches.d.ts +25 -0
- package/dist/branches.js +81 -0
- package/dist/branches.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -0
- package/dist/codesearch.d.ts +27 -0
- package/dist/codesearch.js +111 -0
- package/dist/codesearch.js.map +1 -0
- package/dist/concurrency.d.ts +14 -0
- package/dist/concurrency.js +52 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.d.ts +106 -0
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -0
- package/dist/engine-antigravity.d.ts +23 -0
- package/dist/engine-antigravity.js +357 -0
- package/dist/engine-antigravity.js.map +1 -0
- package/dist/engine-claude.d.ts +18 -0
- package/dist/engine-claude.js +64 -0
- package/dist/engine-claude.js.map +1 -0
- package/dist/engine.d.ts +122 -0
- package/dist/engine.js +36 -0
- package/dist/engine.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +10 -0
- package/dist/env.js.map +1 -0
- package/dist/environment.d.ts +39 -0
- package/dist/environment.js +100 -0
- package/dist/environment.js.map +1 -0
- package/dist/flaredeck-check.d.ts +21 -0
- package/dist/flaredeck-check.js +12 -0
- package/dist/flaredeck-check.js.map +1 -0
- package/dist/flaredeck-proxy.d.ts +60 -0
- package/dist/flaredeck-proxy.js +186 -0
- package/dist/flaredeck-proxy.js.map +1 -0
- package/dist/flaredeck.d.ts +132 -0
- package/dist/flaredeck.js +106 -0
- package/dist/flaredeck.js.map +1 -0
- package/dist/gemini.d.ts +21 -0
- package/dist/gemini.js +59 -0
- package/dist/gemini.js.map +1 -0
- package/dist/gitguard.d.ts +2 -0
- package/dist/gitguard.js +90 -0
- package/dist/gitguard.js.map +1 -0
- package/dist/github.d.ts +38 -0
- package/dist/github.js +153 -0
- package/dist/github.js.map +1 -0
- package/dist/githubauth.d.ts +21 -0
- package/dist/githubauth.js +90 -0
- package/dist/githubauth.js.map +1 -0
- package/dist/intake.d.ts +124 -0
- package/dist/intake.js +383 -0
- package/dist/intake.js.map +1 -0
- package/dist/jam.d.ts +45 -0
- package/dist/jam.js +385 -0
- package/dist/jam.js.map +1 -0
- package/dist/jev.d.ts +31 -0
- package/dist/jev.js +75 -0
- package/dist/jev.js.map +1 -0
- package/dist/jevguard.d.ts +28 -0
- package/dist/jevguard.js +213 -0
- package/dist/jevguard.js.map +1 -0
- package/dist/mailbox.d.ts +22 -0
- package/dist/mailbox.js +51 -0
- package/dist/mailbox.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +764 -0
- package/dist/main.js.map +1 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.js +81 -0
- package/dist/markdown.js.map +1 -0
- package/dist/oauth.d.ts +30 -0
- package/dist/oauth.js +216 -0
- package/dist/oauth.js.map +1 -0
- package/dist/permissions.d.ts +43 -0
- package/dist/permissions.js +57 -0
- package/dist/permissions.js.map +1 -0
- package/dist/preflight.d.ts +71 -0
- package/dist/preflight.js +116 -0
- package/dist/preflight.js.map +1 -0
- package/dist/providers.d.ts +26 -0
- package/dist/providers.js +27 -0
- package/dist/providers.js.map +1 -0
- package/dist/repopicker.d.ts +3 -0
- package/dist/repopicker.js +111 -0
- package/dist/repopicker.js.map +1 -0
- package/dist/runs.d.ts +59 -0
- package/dist/runs.js +211 -0
- package/dist/runs.js.map +1 -0
- package/dist/sandbox.d.ts +13 -0
- package/dist/sandbox.js +110 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schedules.d.ts +11 -0
- package/dist/schedules.js +85 -0
- package/dist/schedules.js.map +1 -0
- package/dist/secretguard.d.ts +4 -0
- package/dist/secretguard.js +115 -0
- package/dist/secretguard.js.map +1 -0
- package/dist/secrets.d.ts +27 -0
- package/dist/secrets.js +51 -0
- package/dist/secrets.js.map +1 -0
- package/dist/serve.d.ts +17 -0
- package/dist/serve.js +1206 -0
- package/dist/serve.js.map +1 -0
- package/dist/status.d.ts +73 -0
- package/dist/status.js +98 -0
- package/dist/status.js.map +1 -0
- package/dist/stream.d.ts +91 -0
- package/dist/stream.js +103 -0
- package/dist/stream.js.map +1 -0
- package/dist/streamtoken.d.ts +2 -0
- package/dist/streamtoken.js +30 -0
- package/dist/streamtoken.js.map +1 -0
- package/dist/team.d.ts +61 -0
- package/dist/team.js +136 -0
- package/dist/team.js.map +1 -0
- package/dist/ui.d.ts +110 -0
- package/dist/ui.js +348 -0
- package/dist/ui.js.map +1 -0
- package/dist/updater.d.ts +15 -0
- package/dist/updater.js +178 -0
- package/dist/updater.js.map +1 -0
- package/dist/workspace.d.ts +115 -0
- package/dist/workspace.js +247 -0
- package/dist/workspace.js.map +1 -0
- package/dist/worktree.d.ts +42 -0
- package/dist/worktree.js +140 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +52 -0
- package/python/sidecar.py +178 -0
package/dist/worktree.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const git = (args, cwd) => new Promise((resolve) => {
|
|
5
|
+
const child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
6
|
+
let out = '';
|
|
7
|
+
const cap = (d) => { if (out.length < 4000)
|
|
8
|
+
out += String(d); };
|
|
9
|
+
child.stdout.on('data', cap);
|
|
10
|
+
child.stderr.on('data', cap);
|
|
11
|
+
child.on('close', (c) => resolve({ code: c ?? 1, out: out.trim() }));
|
|
12
|
+
child.on('error', (e) => resolve({ code: 1, out: String(e) }));
|
|
13
|
+
});
|
|
14
|
+
/** `BIR-0282` → `agent/bir-0282`. Stable, so re-work on a card returns to the same branch. */
|
|
15
|
+
const branchFor = (card) => `agent/${card.toLowerCase().replace(/[^a-z0-9._-]+/g, '-')}`;
|
|
16
|
+
export async function ensureWorktree(repo, card, onStep,
|
|
17
|
+
/**
|
|
18
|
+
* Where work is cut FROM, which is not always the repo's default branch. Some projects have agents
|
|
19
|
+
* branch from `dev` — the lead owns it, agents take the latest and hand a feature branch back.
|
|
20
|
+
* Cutting from the default instead would base every card on the wrong history, and the merge
|
|
21
|
+
* would look like a much bigger change than it is.
|
|
22
|
+
*/
|
|
23
|
+
base) {
|
|
24
|
+
if (!existsSync(join(repo, '.git')))
|
|
25
|
+
return null; // not a git repo; work in place
|
|
26
|
+
const branch = branchFor(card);
|
|
27
|
+
// Beside the repo, not inside it: a worktree within the working tree confuses status, ignore
|
|
28
|
+
// rules and every tool that walks the directory.
|
|
29
|
+
const path = join(repo, '..', `.worktrees-${basenameOf(repo)}`, branch.replace(/\//g, '-'));
|
|
30
|
+
if (existsSync(path)) {
|
|
31
|
+
onStep(`reusing worktree ${branch}`);
|
|
32
|
+
return { path, branch, created: false };
|
|
33
|
+
}
|
|
34
|
+
// Branch from the default branch as the REMOTE has it, so an agent does not inherit whatever
|
|
35
|
+
// half-finished state a local checkout happens to be in. A repo with no remote is a normal
|
|
36
|
+
// case though — a scratch project, a fresh init — so fall back to the local branch rather
|
|
37
|
+
// than giving up on isolation entirely, which is the part that matters.
|
|
38
|
+
const fallback = await defaultBranch(repo);
|
|
39
|
+
const from = base ?? fallback;
|
|
40
|
+
await git(['fetch', '--quiet', 'origin', from], repo);
|
|
41
|
+
const exists = async (ref) => (await git(['rev-parse', '--verify', ref], repo)).code === 0;
|
|
42
|
+
let start = (await exists(`origin/${from}`)) ? `origin/${from}` : (await exists(from)) ? from : '';
|
|
43
|
+
if (!start) {
|
|
44
|
+
// A declared base that does not exist is a configuration mistake, and it used to be absorbed
|
|
45
|
+
// silently: the branch fell through to HEAD, meaning whatever this checkout happened to be
|
|
46
|
+
// sitting on — someone else's half-finished branch as easily as anything. The card then looked
|
|
47
|
+
// normal and its diff was against the wrong history. Say it, and fall back to the repo's own
|
|
48
|
+
// default rather than to an accident.
|
|
49
|
+
onStep(`base "${from}" is not on origin or local — falling back to ${fallback}`);
|
|
50
|
+
start = (await exists(`origin/${fallback}`)) ? `origin/${fallback}` : (await exists(fallback)) ? fallback : 'HEAD';
|
|
51
|
+
}
|
|
52
|
+
const made = await git(['worktree', 'add', '-B', branch, path, start], repo);
|
|
53
|
+
if (made.code !== 0) {
|
|
54
|
+
onStep(`could not make a worktree: ${made.out.split('\n').slice(-1)[0]}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// Make `origin` fetch-only IN THIS WORKTREE.
|
|
58
|
+
//
|
|
59
|
+
// Up to here, "do not publish from the shell" was a PreToolUse hook reading command text, and
|
|
60
|
+
// it says itself that nothing reading strings can be airtight: `git -C`, an alias, a line
|
|
61
|
+
// inside a script. What made that a real hole rather than a theoretical one is the machine —
|
|
62
|
+
// Xcode ships a system gitconfig with `credential.helper = osxkeychain`, and `gh` is logged in
|
|
63
|
+
// with `repo` and `workflow` scope, so any push that got past the regex would quietly find a
|
|
64
|
+
// working credential and succeed.
|
|
65
|
+
//
|
|
66
|
+
// A dead pushurl closes it without reading anything: `git push`, `git push origin`, `git push
|
|
67
|
+
// -f origin main` all fail immediately in this worktree, however they are spelled and whatever
|
|
68
|
+
// wrapper they are spelled inside. Fetch is untouched, so the agent can still stay current
|
|
69
|
+
// with the base branch, which is work it legitimately does. And push_branch is unaffected
|
|
70
|
+
// because it never pushes to `origin`: it builds a one-shot authenticated URL of its own.
|
|
71
|
+
//
|
|
72
|
+
// This is not the fence either — branch protection on the remote is. It is the difference
|
|
73
|
+
// between a door that is asked not to be opened and one with no handle on this side.
|
|
74
|
+
const pushBlocked = await git(['config', '--local', 'remote.origin.pushurl', 'no-push/publish-with-the-push_branch-tool'], path);
|
|
75
|
+
if (pushBlocked.code !== 0) {
|
|
76
|
+
// Said out loud rather than swallowed: the hook still refuses shell pushes, but this
|
|
77
|
+
// worktree is one layer thinner than the others and that is worth knowing.
|
|
78
|
+
onStep(`could not make origin fetch-only here — shell pushes are blocked by the guard only`);
|
|
79
|
+
}
|
|
80
|
+
onStep(`worktree ${branch} from ${start}`);
|
|
81
|
+
return { path, branch, created: true, from: start };
|
|
82
|
+
}
|
|
83
|
+
async function defaultBranch(repo) {
|
|
84
|
+
const head = await git(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], repo);
|
|
85
|
+
if (head.code === 0 && head.out)
|
|
86
|
+
return head.out.replace(/^origin\//, '');
|
|
87
|
+
for (const guess of ['main', 'master', 'dev']) {
|
|
88
|
+
if ((await git(['rev-parse', '--verify', `origin/${guess}`], repo)).code === 0)
|
|
89
|
+
return guess;
|
|
90
|
+
}
|
|
91
|
+
return 'main';
|
|
92
|
+
}
|
|
93
|
+
const basenameOf = (p) => p.replace(/\/+$/, '').split('/').pop() || 'repo';
|
|
94
|
+
/** Told to the agent so it knows where it is and what it is on. */
|
|
95
|
+
export function worktreeBrief(tree) {
|
|
96
|
+
if (!tree)
|
|
97
|
+
return '';
|
|
98
|
+
return (`\n\nYou are working in ${tree.path} on branch ${tree.branch}` +
|
|
99
|
+
`${tree.from ? `, branched from ${tree.from}` : ''} — a checkout of your own. Commit here as ` +
|
|
100
|
+
`you go, then publish with push_branch. Do not touch any other checkout on this machine: ` +
|
|
101
|
+
`someone may be using it.`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove a card's worktree once the card is finished with.
|
|
105
|
+
*
|
|
106
|
+
* Worktrees accumulate: one repository here had fourteen, most of them long-done cards, each a
|
|
107
|
+
* full checkout on disk. But a worktree is also the ONLY copy of anything an agent has committed
|
|
108
|
+
* and not published, so this refuses far more readily than it deletes.
|
|
109
|
+
*
|
|
110
|
+
* It removes only when there is nothing to lose: no uncommitted changes, no untracked files, and
|
|
111
|
+
* no commit that is not already on the remote. Anything else is left exactly where it is, and the
|
|
112
|
+
* reason is returned so a person can be told rather than left to find an orphan later.
|
|
113
|
+
*/
|
|
114
|
+
export async function releaseWorktree(repo, card, onStep) {
|
|
115
|
+
const branch = branchFor(card);
|
|
116
|
+
const path = join(repo, '..', `.worktrees-${basenameOf(repo)}`, branch.replace(/\//g, '-'));
|
|
117
|
+
if (!existsSync(path))
|
|
118
|
+
return { removed: false, why: 'there was no worktree' };
|
|
119
|
+
const dirty = await git(['status', '--porcelain'], path);
|
|
120
|
+
if (dirty.code !== 0)
|
|
121
|
+
return { removed: false, why: `could not read its status (${dirty.out.split('\n')[0]})` };
|
|
122
|
+
if (dirty.out.trim())
|
|
123
|
+
return { removed: false, why: 'it has uncommitted changes' };
|
|
124
|
+
// Commits that exist here and nowhere else. `--remotes` covers a branch pushed under any name,
|
|
125
|
+
// so a published branch counts as safe however push_branch named it.
|
|
126
|
+
const unpushed = await git(['log', '--oneline', '--not', '--remotes'], path);
|
|
127
|
+
if (unpushed.code === 0 && unpushed.out.trim()) {
|
|
128
|
+
const n = unpushed.out.trim().split('\n').length;
|
|
129
|
+
return { removed: false, why: `it has ${n} commit${n === 1 ? '' : 's'} that are not on the remote` };
|
|
130
|
+
}
|
|
131
|
+
const gone = await git(['worktree', 'remove', '--force', path], repo);
|
|
132
|
+
if (gone.code !== 0)
|
|
133
|
+
return { removed: false, why: `git would not remove it (${gone.out.split('\n')[0]})` };
|
|
134
|
+
// The branch too — it pointed at the same published commits, and leaving hundreds of agent/*
|
|
135
|
+
// branches behind makes a real branch list unreadable.
|
|
136
|
+
await git(['branch', '-D', branch], repo);
|
|
137
|
+
onStep(`released worktree ${branch}`);
|
|
138
|
+
return { removed: true, why: 'nothing was left on it' };
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=worktree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree.js","sourceRoot":"","sources":["../src/worktree.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onepointfour-packs/flaredeck-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run a FlareDeck agent on your own machine: it picks up cards assigned to it, works each one in a checkout of its own, and reports back on the card.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flaredeck-agent": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"python",
|
|
12
|
+
"LICENSE.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsx src/cli.ts",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.283",
|
|
24
|
+
"ink": "^7.1.1",
|
|
25
|
+
"ink-text-input": "^6.0.0",
|
|
26
|
+
"react": "^19.3.0",
|
|
27
|
+
"yaml": "^2.9.1",
|
|
28
|
+
"zod": "^4.6.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.10.2",
|
|
32
|
+
"@types/react": "^19.3.0",
|
|
33
|
+
"tsx": "^4.19.2",
|
|
34
|
+
"typescript": "^5.7.2"
|
|
35
|
+
},
|
|
36
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
37
|
+
"homepage": "https://app.flaredeck.app",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://app.flaredeck.app"
|
|
40
|
+
},
|
|
41
|
+
"author": "Phiresky",
|
|
42
|
+
"keywords": [
|
|
43
|
+
"flaredeck",
|
|
44
|
+
"agent",
|
|
45
|
+
"kanban",
|
|
46
|
+
"mcp",
|
|
47
|
+
"claude",
|
|
48
|
+
"gemini",
|
|
49
|
+
"automation",
|
|
50
|
+
"coding-agent"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""The Gemini half of the harness.
|
|
2
|
+
|
|
3
|
+
The harness is TypeScript; the Antigravity SDK is Python with a compiled runtime binary, so this
|
|
4
|
+
runs as a child process and speaks JSON Lines over stdin/stdout. It is deliberately thin: it owns
|
|
5
|
+
no policy and makes no decisions. Every tool call is referred back to Node over localhost, so
|
|
6
|
+
gitGuard, secretGuard and the Jev gate keep ONE implementation rather than being rewritten here
|
|
7
|
+
and drifting from the originals.
|
|
8
|
+
|
|
9
|
+
Node → here {"type":"run","brief":...,"cwd":...,"resume":...,"model":...,"guard_url":...}
|
|
10
|
+
{"type":"steer","text":...}
|
|
11
|
+
{"type":"stop"}
|
|
12
|
+
here → Node {"type":"event","kind":"text"|"tool"|"todo"|"usage"|"done"|"ready", ...}
|
|
13
|
+
{"type":"fatal","message":...}
|
|
14
|
+
|
|
15
|
+
The event names are the harness's own `AgentEvent` union, not Antigravity's, because that union
|
|
16
|
+
was written for its consumers and already has no vendor in it.
|
|
17
|
+
"""
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import urllib.request
|
|
23
|
+
|
|
24
|
+
from contextlib import AsyncExitStack
|
|
25
|
+
|
|
26
|
+
from google.antigravity import Agent, LocalAgentConfig
|
|
27
|
+
from google.antigravity.hooks import policy, post_tool_call, pre_tool_call_decide
|
|
28
|
+
from google.antigravity.types import HookResult, McpStreamableHttpServer
|
|
29
|
+
|
|
30
|
+
GUARD_URL = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def emit(obj: dict) -> None:
|
|
34
|
+
"""One JSON object per line, flushed — Node is reading this as a stream, not a file."""
|
|
35
|
+
sys.stdout.write(json.dumps(obj) + "\n")
|
|
36
|
+
sys.stdout.flush()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ask_node(path: str, payload: dict) -> dict:
|
|
40
|
+
"""Put the decision where the policy already lives. A guard that cannot be reached DENIES:
|
|
41
|
+
an agent that keeps working because its supervisor is unreachable is the failure this
|
|
42
|
+
harness exists to prevent."""
|
|
43
|
+
try:
|
|
44
|
+
req = urllib.request.Request(
|
|
45
|
+
f"{GUARD_URL}{path}",
|
|
46
|
+
data=json.dumps(payload).encode(),
|
|
47
|
+
headers={"Content-Type": "application/json"},
|
|
48
|
+
)
|
|
49
|
+
with urllib.request.urlopen(req, timeout=120) as r:
|
|
50
|
+
return json.loads(r.read().decode())
|
|
51
|
+
except Exception as e: # noqa: BLE001 - any failure here must be closed, not loud
|
|
52
|
+
return {"allow": False, "message": f"The harness could not be reached to approve this ({e})."}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pre_tool_call_decide
|
|
56
|
+
async def gate(data):
|
|
57
|
+
"""Antigravity's shell tool is `run_command` and its argument is `CommandLine` — NOT
|
|
58
|
+
`command`. Reading the wrong key yields an empty string, which every guard would wave
|
|
59
|
+
through, so the name is asserted rather than assumed."""
|
|
60
|
+
args = dict(getattr(data, "args", {}) or {})
|
|
61
|
+
verdict = ask_node("/tool-call", {
|
|
62
|
+
"name": str(getattr(data, "name", "")),
|
|
63
|
+
# Which server a tool came from: the harness pre-approves FlareDeck's READ tools the same
|
|
64
|
+
# way it does for the other engine, and cannot tell them apart without this.
|
|
65
|
+
"server": str(getattr(data, "server_name", "") or ""),
|
|
66
|
+
"args": args,
|
|
67
|
+
})
|
|
68
|
+
if not verdict.get("allow", False):
|
|
69
|
+
return HookResult(allow=False, message=verdict.get("message", "Refused by the harness."))
|
|
70
|
+
modified = verdict.get("modified_args")
|
|
71
|
+
if modified:
|
|
72
|
+
return HookResult(allow=True, modified_args=modified)
|
|
73
|
+
return HookResult(allow=True)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@post_tool_call
|
|
77
|
+
async def seen(data):
|
|
78
|
+
"""PostToolCallHook is an InspectHook: it can SEE a result and cannot change it. So this
|
|
79
|
+
reports a secret that came back rather than redacting it — the harness decides what to do
|
|
80
|
+
about an engine that cannot redact, and says so out loud rather than pretending."""
|
|
81
|
+
result = getattr(data, "result", None)
|
|
82
|
+
if result is not None:
|
|
83
|
+
ask_node("/tool-result", {"name": str(getattr(data, "name", "")), "result": str(result)[:20000]})
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def main() -> None:
|
|
88
|
+
global GUARD_URL
|
|
89
|
+
first = json.loads(sys.stdin.readline())
|
|
90
|
+
if first.get("type") != "run":
|
|
91
|
+
emit({"type": "fatal", "message": f"expected a run message, got {first.get('type')!r}"})
|
|
92
|
+
return
|
|
93
|
+
GUARD_URL = first["guard_url"]
|
|
94
|
+
|
|
95
|
+
kwargs = {
|
|
96
|
+
"system_instructions": first.get("system_instructions") or "",
|
|
97
|
+
"policies": [policy.allow_all()], # the gate above is the policy; this just defers to it
|
|
98
|
+
"hooks": [gate, seen],
|
|
99
|
+
"workspaces": [first["cwd"]],
|
|
100
|
+
}
|
|
101
|
+
# FlareDeck, reached over HTTP with the HARNESS's own token — so the model's tools act as the
|
|
102
|
+
# same account the harness listens as. Two identities for one agent is a thing that has gone
|
|
103
|
+
# wrong here before and is hard to see once it has.
|
|
104
|
+
mcp = [
|
|
105
|
+
McpStreamableHttpServer(name=m["name"], url=m["url"], headers=m.get("headers") or {})
|
|
106
|
+
for m in (first.get("mcp") or [])
|
|
107
|
+
]
|
|
108
|
+
if mcp:
|
|
109
|
+
kwargs["mcp_servers"] = mcp
|
|
110
|
+
if os.environ.get("GOOGLE_GENAI_USE_VERTEXAI"):
|
|
111
|
+
kwargs.update(vertex=True, project=os.environ["GOOGLE_CLOUD_PROJECT"],
|
|
112
|
+
location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"))
|
|
113
|
+
else:
|
|
114
|
+
kwargs["api_key"] = os.environ["GEMINI_API_KEY"]
|
|
115
|
+
if first.get("model"):
|
|
116
|
+
kwargs["model"] = first["model"]
|
|
117
|
+
if first.get("resume"):
|
|
118
|
+
kwargs["conversation_id"] = first["resume"]
|
|
119
|
+
|
|
120
|
+
# A conversation lives in the Antigravity runtime, which dies with this process — so a session
|
|
121
|
+
# id the harness stored before a restart names something that no longer exists, and asking for
|
|
122
|
+
# it fails the whole card: "Failed to create agent: conversation ... not found". The card is
|
|
123
|
+
# not lost by that. Its whole thread is on FlareDeck and it is perfectly workable from a fresh
|
|
124
|
+
# conversation, so an unusable resume is dropped rather than raised.
|
|
125
|
+
#
|
|
126
|
+
# Both the construction AND the entering are covered, because the runtime is only reached on
|
|
127
|
+
# entry — that is where the failure actually surfaces.
|
|
128
|
+
stack = AsyncExitStack()
|
|
129
|
+
try:
|
|
130
|
+
agent = await stack.enter_async_context(Agent(LocalAgentConfig(**kwargs)))
|
|
131
|
+
except Exception as e: # noqa: BLE001
|
|
132
|
+
if not kwargs.get("conversation_id") or "not found" not in str(e).lower():
|
|
133
|
+
raise
|
|
134
|
+
gone = str(kwargs.pop("conversation_id"))[:8]
|
|
135
|
+
await stack.aclose()
|
|
136
|
+
# Said out loud: an agent that silently forgot a conversation would re-read the card and
|
|
137
|
+
# quietly redo work it had already reported.
|
|
138
|
+
emit({"type": "event", "kind": "text",
|
|
139
|
+
"text": f"The earlier session for this card is gone ({gone}…), so this starts fresh from the card and its comments."})
|
|
140
|
+
stack = AsyncExitStack()
|
|
141
|
+
agent = await stack.enter_async_context(Agent(LocalAgentConfig(**kwargs)))
|
|
142
|
+
|
|
143
|
+
async with stack:
|
|
144
|
+
# "connected", not "configured", and it is not a guess: a server that fails to load raises
|
|
145
|
+
# during Agent construction ("MCP load failed for flaredeck: ..."), so reaching this line
|
|
146
|
+
# means every server in `mcp` is really attached to THIS session. The harness refuses to
|
|
147
|
+
# work a card otherwise, and it is right to — the tool names come from the harness's own
|
|
148
|
+
# discovery a moment earlier, because the SDK gives no way to enumerate a session's tools.
|
|
149
|
+
emit({"type": "event", "kind": "ready", "tools": first.get("tool_names") or [],
|
|
150
|
+
"mcp": [{"name": m.name, "status": "connected"} for m in mcp],
|
|
151
|
+
"apiKeySource": "vertex" if kwargs.get("vertex") else "gemini-api-key",
|
|
152
|
+
"permissionMode": "harness-gated",
|
|
153
|
+
"sessionId": str(getattr(agent, "conversation_id", "") or "")})
|
|
154
|
+
response = await agent.chat(first["brief"])
|
|
155
|
+
text = await response.text()
|
|
156
|
+
if text:
|
|
157
|
+
emit({"type": "event", "kind": "text", "text": text})
|
|
158
|
+
# The session id only exists AFTER the first turn — conversation_id is None until then —
|
|
159
|
+
# so the ready event above could never carry it, and the harness, finding no session,
|
|
160
|
+
# skipped the handover entirely: "no session to write a handover from". That handover is
|
|
161
|
+
# where an agent writes its notes and closes the card, so the work half ran and the
|
|
162
|
+
# report half silently never did. Sending ready again with the real id is enough: the
|
|
163
|
+
# harness stores a session id from any ready that has one.
|
|
164
|
+
session_id = str(getattr(agent, "conversation_id", "") or "")
|
|
165
|
+
if session_id:
|
|
166
|
+
emit({"type": "event", "kind": "ready", "tools": first.get("tool_names") or [],
|
|
167
|
+
"mcp": [{"name": m.name, "status": "connected"} for m in mcp],
|
|
168
|
+
"apiKeySource": "vertex" if kwargs.get("vertex") else "gemini-api-key",
|
|
169
|
+
"permissionMode": "harness-gated", "sessionId": session_id})
|
|
170
|
+
emit({"type": "event", "kind": "done", "ok": True, "result": text or ""})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
try:
|
|
175
|
+
asyncio.run(main())
|
|
176
|
+
except Exception as e: # noqa: BLE001
|
|
177
|
+
emit({"type": "fatal", "message": str(e)})
|
|
178
|
+
sys.exit(1)
|