granttap-mcp 0.8.106 → 0.8.109
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/apps/bridge/src/codex-desktop/README.md +5 -0
- package/apps/bridge/src/codex-desktop/connection.ts +3 -1
- package/apps/bridge/src/device-management/cache.ts +73 -0
- package/apps/bridge/src/device-management/index.ts +88 -0
- package/apps/bridge/src/device-management/login.ts +110 -0
- package/apps/bridge/src/device-management/maintenance.ts +91 -0
- package/apps/bridge/src/device-management/provider-command.ts +89 -0
- package/apps/bridge/src/engine/README.md +2 -1
- package/apps/bridge/src/engine/distribution/artifacts.ts +2 -2
- package/apps/bridge/src/mesh/restrictions/check.ts +3 -2
- package/apps/bridge/src/mesh/restrictions/cli.ts +6 -2
- package/apps/bridge/src/mesh/restrictions/evaluate.ts +1 -1
- package/apps/bridge/src/mesh/restrictions/guidance/README.md +27 -0
- package/apps/bridge/src/mesh/restrictions/guidance/index.ts +45 -0
- package/apps/bridge/src/mesh/restrictions/store.ts +3 -0
- package/apps/bridge/src/monitor/handlers/inbound.ts +2 -0
- package/apps/bridge/src/monitor/handlers/user-message.ts +4 -1
- package/apps/bridge/src/monitor/support/runtime.ts +3 -1
- package/apps/bridge/src/providers/acp-file-changes/README.md +10 -0
- package/apps/bridge/src/providers/acp-file-changes/index.ts +27 -0
- package/apps/bridge/src/providers/grok-conversation/README.md +21 -0
- package/apps/bridge/src/providers/grok-conversation/index.ts +66 -0
- package/apps/bridge/src/providers/grok-conversation/projection.ts +120 -0
- package/apps/bridge/src/providers/grok-tools/README.md +22 -0
- package/apps/bridge/src/providers/grok-tools/changes.ts +28 -0
- package/apps/bridge/src/providers/grok-tools/events.ts +59 -0
- package/apps/bridge/src/providers/grok-tools/index.ts +55 -0
- package/apps/bridge/src/reply/cursor-conversation/README.md +5 -0
- package/apps/bridge/src/reply/cursor-conversation/index.ts +9 -2
- package/apps/bridge/src/reply/index.ts +10 -4
- package/apps/bridge/src/reply/payload/routing.ts +3 -1
- package/apps/bridge/src/reply/process/auth-failure.ts +11 -0
- package/apps/bridge/src/reply/process/index.ts +24 -2
- package/apps/bridge/src/reply/process/provider-headless.ts +2 -1
- package/apps/bridge/src/runtime-updates/index.ts +6 -5
- package/apps/bridge/src/sessions/cursor/conversations/README.md +5 -0
- package/apps/bridge/src/sessions/cursor/conversations/history.ts +5 -2
- package/apps/bridge/src/sessions/cursor/conversations/index.ts +19 -3
- package/apps/bridge/src/sessions/cursor/conversations/tools.ts +36 -0
- package/apps/bridge/src/sessions/scan/grok.ts +21 -14
- package/apps/bridge/src/sessions/support/activity-helpers.ts +1 -0
- package/apps/bridge/src/task-dispatch/README.md +15 -0
- package/apps/bridge/src/task-dispatch/sender/index.ts +9 -4
- package/apps/bridge/src/task-dispatch/worker/monitor.ts +5 -0
- package/apps/bridge/src/task-dispatch/worker/readiness.ts +8 -1
- package/apps/bridge/src/task-transfer/README.md +14 -0
- package/apps/bridge/src/task-transfer/context.ts +11 -3
- package/apps/bridge/src/task-transfer/persistence.ts +3 -2
- package/apps/bridge/src/task-transfer/runtime/local.ts +2 -1
- package/apps/mcp/src/desktop/README.md +4 -0
- package/apps/mcp/src/desktop/task-activity-runner.ts +6 -3
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/docs/device-management.md +45 -0
- package/package.json +1 -1
- package/packages/protocol/messages/device/management.ts +73 -0
- package/packages/protocol/messages/project-policy.ts +2 -39
- package/packages/protocol/messages/restrictions/index.ts +51 -0
- package/packages/protocol/schema.ts +4 -0
|
@@ -34,3 +34,8 @@ Document staging for a native-only question keeps the file note inside its
|
|
|
34
34
|
original answer envelope. The host's native question and all paired controllers
|
|
35
35
|
therefore observe the same question item as answered; document paths remain
|
|
36
36
|
available to the admitted turn. Ordinary chat text is unchanged.
|
|
37
|
+
|
|
38
|
+
The bounded native admission deadline leaves time for the installed router’s
|
|
39
|
+
ten-second owner discovery window. A five-second client cap otherwise reports
|
|
40
|
+
healthy CLI conversations unavailable before discovery can finish. Missing-owner
|
|
41
|
+
confirmation preserves CLI continuation; uncertain dispatch remains unreplayed.
|
|
@@ -17,7 +17,9 @@ export class DesktopConnection {
|
|
|
17
17
|
private readonly deadline: number;
|
|
18
18
|
|
|
19
19
|
constructor(path: string, timeoutMs: number) {
|
|
20
|
-
|
|
20
|
+
// The native router allows ten seconds for owner discovery. Keep time
|
|
21
|
+
// for initialization, discovery and its routed response within the turn.
|
|
22
|
+
this.deadline = Date.now() + Math.min(30_000, timeoutMs);
|
|
21
23
|
this.socket = connect(path);
|
|
22
24
|
this.socket.on('error', () => this.close());
|
|
23
25
|
this.socket.on('close', () => this.close());
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { lstat, readdir, rm, mkdir, open, unlink } from 'node:fs/promises';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { readState, stableVersion } from '../runtime-updates/selection.mjs';
|
|
6
|
+
import { configDir } from '../config/runtime/paths';
|
|
7
|
+
|
|
8
|
+
type Category = { id: string; name: string; bytes: number; reclaimableBytes: number; paths: string[] };
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
async function bytes(path: string): Promise<number> {
|
|
12
|
+
try {
|
|
13
|
+
const s = await lstat(path);
|
|
14
|
+
if (s.isSymbolicLink()) return 0;
|
|
15
|
+
if (!s.isDirectory()) return s.isFile() ? s.size : 0;
|
|
16
|
+
let total = 0;
|
|
17
|
+
for (const name of await readdir(path)) total += await bytes(join(path, name));
|
|
18
|
+
return total;
|
|
19
|
+
} catch { return 0; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Only disposable indexes and obsolete immutable runtime packages. Never chats, credentials or projects. */
|
|
23
|
+
export async function computerCache(root: string, directory = configDir(), processes?: string): Promise<Category[]> {
|
|
24
|
+
const graph = join(directory, 'repository-graph-cache');
|
|
25
|
+
const graphBytes = await bytes(graph);
|
|
26
|
+
const categories: Category[] = [{ id: 'graph', name: 'Project indexes', bytes: graphBytes, reclaimableBytes: graphBytes, paths: [graph] }];
|
|
27
|
+
const runtimeDirectory = join(directory, 'runtime-updates');
|
|
28
|
+
const state = readState(runtimeDirectory);
|
|
29
|
+
const protectedRoots = [root, state.root, state.previous?.root].filter((v): v is string => typeof v === 'string').map(v => resolve(v));
|
|
30
|
+
// If process inspection is unavailable, retain every runtime rather than guess.
|
|
31
|
+
if (processes == null) {
|
|
32
|
+
try { processes = (await exec('/bin/ps', ['-axo', 'command='], { maxBuffer: 8 * 1024 * 1024, timeout: 3_000 })).stdout; }
|
|
33
|
+
catch { processes = undefined; }
|
|
34
|
+
}
|
|
35
|
+
const runtime: Category = { id: 'runtimes', name: 'Previous MCP downloads', bytes: 0, reclaimableBytes: 0, paths: [] };
|
|
36
|
+
const versions = join(runtimeDirectory, 'versions');
|
|
37
|
+
for (const entry of await readdir(versions, { withFileTypes: true }).catch(() => [])) {
|
|
38
|
+
if (!entry.isDirectory() || !stableVersion(entry.name)) continue;
|
|
39
|
+
const path = join(versions, entry.name), packageRoot = join(path, 'node_modules', 'granttap-mcp');
|
|
40
|
+
const size = await bytes(path); runtime.bytes += size;
|
|
41
|
+
if (processes != null && !protectedRoots.includes(resolve(packageRoot)) && !processes.includes(path)) {
|
|
42
|
+
runtime.paths.push(path); runtime.reclaimableBytes += size;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
categories.push(runtime);
|
|
46
|
+
return categories;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function clearComputerCache(root: string, directory = configDir()): Promise<number> {
|
|
50
|
+
const lockPath = join(directory, 'runtime-updates', 'install.lock');
|
|
51
|
+
await mkdir(join(directory, 'runtime-updates'), { recursive: true, mode: 0o700 });
|
|
52
|
+
let lock;
|
|
53
|
+
try { lock = await open(lockPath, 'wx', 0o600); }
|
|
54
|
+
catch { throw Error('An MCP install or cache cleanup is in progress. Wait for it to finish.'); }
|
|
55
|
+
try { return await removeCaches(root, directory); }
|
|
56
|
+
finally { await lock.close(); await unlink(lockPath); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function removeCaches(root: string, directory: string): Promise<number> {
|
|
60
|
+
// Re-read selection/process ownership immediately before each deletion.
|
|
61
|
+
const first = await computerCache(root, directory);
|
|
62
|
+
let removed = 0;
|
|
63
|
+
for (const category of first) for (const path of category.paths) {
|
|
64
|
+
const current = await computerCache(root, directory);
|
|
65
|
+
if (!current.some(c => c.paths.includes(path))) continue;
|
|
66
|
+
try {
|
|
67
|
+
if ((await lstat(path)).isSymbolicLink()) continue;
|
|
68
|
+
const size = await bytes(path);
|
|
69
|
+
await rm(path, { recursive: true, force: true }); removed += size;
|
|
70
|
+
} catch { /* A locked cache stays; remaining categories can still be cleared. */ }
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { RelayClient } from '../../../../packages/core/relay-client';
|
|
3
|
+
import type { DeviceManagementRequest, DeviceManagementResult } from '../../../../packages/protocol/schema';
|
|
4
|
+
import { loadConfig, machineConfigPath } from '../config';
|
|
5
|
+
import { observedComputerId } from '../mesh/identity/computer';
|
|
6
|
+
import { clearComputerCache } from './cache';
|
|
7
|
+
import { providerLogins } from './login';
|
|
8
|
+
import { managementSnapshot, maintenanceIdle, restartProvider, restartRuntime, runtimeRoot, updateRuntime } from './maintenance';
|
|
9
|
+
|
|
10
|
+
type Send = (result: DeviceManagementResult) => Promise<void>;
|
|
11
|
+
type Entry = { fingerprint: string; result?: DeviceManagementResult; pending?: Promise<void>; at: number; ttl: number };
|
|
12
|
+
const requests = new Map<string, Entry>();
|
|
13
|
+
let mutating = false;
|
|
14
|
+
export function deviceManagementWorkActive(): boolean { return [...requests.values()].some(entry => entry.pending != null); }
|
|
15
|
+
|
|
16
|
+
export async function handleDeviceManagement(client: RelayClient, request: DeviceManagementRequest,
|
|
17
|
+
peer: string | undefined): Promise<boolean> {
|
|
18
|
+
if (!peer) return false;
|
|
19
|
+
if (!Number.isFinite(request.createdAt) || Math.abs(Date.now() - request.createdAt) > 120_000) return false;
|
|
20
|
+
const machine = loadConfig(machineConfigPath());
|
|
21
|
+
if (machine.role !== 'machine' || machine.room !== client.room
|
|
22
|
+
|| (machine.peerPublicKey !== peer && !machine.extraPeerPublicKeys?.includes(peer))) return false;
|
|
23
|
+
const computerId = observedComputerId();
|
|
24
|
+
if (request.computerId != null && request.computerId !== computerId) return false;
|
|
25
|
+
// Sign-in material is deliberately non-durable and addressed only to the requesting paired controller.
|
|
26
|
+
const send: Send = value => client.sendToPeer(value, peer, { reliable: false, ttlMs: 60_000 });
|
|
27
|
+
const key = client.room + ':' + peer + ':' + request.requestId;
|
|
28
|
+
const fingerprint = createHash('sha256').update(JSON.stringify(request)).digest('hex');
|
|
29
|
+
for (const [k, value] of requests) if (Date.now() - value.at > value.ttl && !value.pending) requests.delete(k);
|
|
30
|
+
const previous = requests.get(key);
|
|
31
|
+
if (previous) {
|
|
32
|
+
if (previous.fingerprint !== fingerprint) return false;
|
|
33
|
+
if (previous.pending) await previous.pending;
|
|
34
|
+
if (previous.result) await send(previous.result);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (requests.size >= 128) return false;
|
|
38
|
+
const entry: Entry = { fingerprint, at: Date.now(), ttl: request.action === 'status' ? 60_000 : 15 * 60_000 };
|
|
39
|
+
requests.set(key, entry);
|
|
40
|
+
const report: Send = async result => { entry.result = result; await send(result); };
|
|
41
|
+
entry.pending = execute(request, client.room + ':' + peer, computerId, report)
|
|
42
|
+
.catch(async () => { /* disconnected phones recover with a new status request */ })
|
|
43
|
+
.finally(() => { entry.pending = undefined; });
|
|
44
|
+
// Commands run independently of message delivery; repeated taps reuse their operation.
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function execute(request: DeviceManagementRequest, scope: string, computerId: string, send: Send): Promise<void> {
|
|
49
|
+
const base = { type: 'device.manage.result' as const, requestId: request.requestId, computerId,
|
|
50
|
+
action: request.action, agent: request.agent };
|
|
51
|
+
const result = (phase: DeviceManagementResult['phase'], message: string): DeviceManagementResult =>
|
|
52
|
+
({ ...base, phase, message, createdAt: Date.now() });
|
|
53
|
+
const exclusive = request.action !== 'status' && !request.action.startsWith('provider-login');
|
|
54
|
+
if (exclusive && mutating) { await send(result('failed', 'Another computer maintenance action is in progress.')); return; }
|
|
55
|
+
if (exclusive) mutating = true;
|
|
56
|
+
try {
|
|
57
|
+
if (request.action.startsWith('provider-login')) {
|
|
58
|
+
const emit = (login: NonNullable<DeviceManagementResult['login']>) => {
|
|
59
|
+
const phase = login.phase === 'signed-in' || login.phase === 'cancelled' ? 'completed'
|
|
60
|
+
: login.phase === 'failed' || login.phase === 'expired' ? 'failed' : 'waiting';
|
|
61
|
+
void send({ ...result(phase, login.message), login }).catch(() => {});
|
|
62
|
+
};
|
|
63
|
+
const login = request.action === 'provider-login' ? providerLogins.start(scope, request.agent!, emit)
|
|
64
|
+
: request.action === 'provider-login-code' ? providerLogins.code(scope, request.agent!, request.operationId!, request.code!)
|
|
65
|
+
: providerLogins.cancel(scope, request.agent!, request.operationId ?? '');
|
|
66
|
+
await send({ ...result('waiting', login.message), login }); return;
|
|
67
|
+
}
|
|
68
|
+
await send(result('running', request.action === 'status' ? 'Reading this computer…' : 'Working on this computer…'));
|
|
69
|
+
let message = 'Computer status refreshed.';
|
|
70
|
+
if (request.action === 'clear-cache') {
|
|
71
|
+
if (!maintenanceIdle()) throw Error('Finish active requests and sign-in before clearing computer caches.');
|
|
72
|
+
const bytes = await clearComputerCache(runtimeRoot);
|
|
73
|
+
message = `Cleared ${(bytes / 1024 / 1024).toFixed(1)} MB of disposable cache.`;
|
|
74
|
+
} else if (request.action === 'runtime-update' || request.action === 'runtime-install') {
|
|
75
|
+
message = await updateRuntime();
|
|
76
|
+
} else if (request.action === 'provider-restart') {
|
|
77
|
+
message = await restartProvider(request.agent!, request.interruptActiveTasks);
|
|
78
|
+
} else if (request.action === 'runtime-restart') {
|
|
79
|
+
// The phone waits for a different runtimeEpoch, rather than treating this acknowledgement as a restart.
|
|
80
|
+
if (!maintenanceIdle()) throw Error('Finish active requests and sign-in before restarting MCP.');
|
|
81
|
+
await send(result('waiting', 'Restart requested. Waiting for MCP to reconnect…'));
|
|
82
|
+
restartRuntime(); return;
|
|
83
|
+
}
|
|
84
|
+
await send({ ...result('completed', message), snapshot: await managementSnapshot() });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await send(result('failed', error instanceof Error ? error.message.slice(0, 512) : 'This action failed on the computer.'));
|
|
87
|
+
} finally { if (exclusive) mutating = false; }
|
|
88
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import type { CodingAgent, ProviderLoginProgress } from '../../../../packages/protocol/schema';
|
|
4
|
+
import { signalOwnedProcess } from '../reply/process';
|
|
5
|
+
import { stripAnsi } from '../tools/version';
|
|
6
|
+
import { allowedLoginURL, installedProvider, loginArguments, providerCommand, providerEnvironment } from './provider-command';
|
|
7
|
+
|
|
8
|
+
type Flow = { scope: string; child: ChildProcess; progress: ProviderLoginProgress; output: string;
|
|
9
|
+
timer: NodeJS.Timeout; emit: (value: ProviderLoginProgress) => void };
|
|
10
|
+
const terminal = new Set(['signed-in', 'failed', 'cancelled', 'expired']);
|
|
11
|
+
|
|
12
|
+
/** Authentication stays in the provider CLI. Only its temporary browser challenge crosses E2EE. */
|
|
13
|
+
export class ProviderLogins {
|
|
14
|
+
private flows = new Map<string, Flow>();
|
|
15
|
+
constructor(private launch = spawn, private command = providerCommand, private now = Date.now) {}
|
|
16
|
+
|
|
17
|
+
active(): boolean { return [...this.flows.values()].some(f => !terminal.has(f.progress.phase)); }
|
|
18
|
+
|
|
19
|
+
start(scope: string, agent: CodingAgent, emit: Flow['emit']): ProviderLoginProgress {
|
|
20
|
+
const existing = [...this.flows.values()].find(f => f.scope === scope && f.progress.agent === agent
|
|
21
|
+
&& !terminal.has(f.progress.phase));
|
|
22
|
+
if (existing) { existing.emit = emit; return existing.progress; }
|
|
23
|
+
if (this.activeFor(agent)) throw Error('A sign-in for this provider is already open on another controller.');
|
|
24
|
+
const command = this.command(agent);
|
|
25
|
+
if (!installedProvider(command)) throw Error('Install this provider on the computer before signing in.');
|
|
26
|
+
this.prune();
|
|
27
|
+
if (this.flows.size >= 8) throw Error('Too many sign-in requests. Wait for an existing sign-in to expire.');
|
|
28
|
+
const progress: ProviderLoginProgress = { operationId: randomUUID(), agent, phase: 'starting',
|
|
29
|
+
expiresAt: this.now() + 10 * 60_000, message: 'Starting provider sign-in on this computer…' };
|
|
30
|
+
const child = this.launch(command, loginArguments(agent), { env: { ...providerEnvironment(), BROWSER: '/usr/bin/false' },
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
|
|
32
|
+
const timer = setTimeout(() => this.finish(progress.operationId, 'expired', 'Sign-in expired. Start again.'), 10 * 60_000);
|
|
33
|
+
timer.unref();
|
|
34
|
+
const flow: Flow = { scope, child, progress, output: '', timer, emit };
|
|
35
|
+
this.flows.set(progress.operationId, flow);
|
|
36
|
+
child.stdout?.setEncoding('utf8'); child.stderr?.setEncoding('utf8');
|
|
37
|
+
child.stdout?.on('data', value => this.output(flow, String(value)));
|
|
38
|
+
child.stderr?.on('data', value => this.output(flow, String(value)));
|
|
39
|
+
child.stdin?.on('error', () => this.finish(progress.operationId, 'failed', 'The provider closed sign-in input. Start again.'));
|
|
40
|
+
child.on('error', () => this.finish(progress.operationId, 'failed', 'The provider could not start sign-in.'));
|
|
41
|
+
child.on('close', code => this.finish(progress.operationId, code === 0 ? 'signed-in' : 'failed',
|
|
42
|
+
code === 0 ? 'Signed in on this computer.' : 'The provider did not complete sign-in. Start again.'));
|
|
43
|
+
return progress;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private activeFor(agent: CodingAgent): boolean {
|
|
47
|
+
return [...this.flows.values()].some(f => f.progress.agent === agent && !terminal.has(f.progress.phase));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private output(flow: Flow, value: string): void {
|
|
51
|
+
if (terminal.has(flow.progress.phase)) return;
|
|
52
|
+
flow.output = stripAnsi(flow.output + value).slice(-65_536);
|
|
53
|
+
const urls = flow.output.match(/https:\/\/[^\s<>"\u001b]+/g) ?? [];
|
|
54
|
+
const verificationURL = urls.map(url => allowedLoginURL(flow.progress.agent, url.replace(/[).,]+$/, ''))).find(Boolean);
|
|
55
|
+
if (!verificationURL) return;
|
|
56
|
+
const queryCode = new URL(verificationURL).searchParams.get('user_code');
|
|
57
|
+
const userCode = queryCode && /^[A-Z0-9-]{3,128}$/i.test(queryCode) ? queryCode
|
|
58
|
+
: /(?:user|device|verification|one.time|enter(?:\s+the)?)?\s*code\s*[:\n]\s*([A-Z0-9]{3,8}(?:-[A-Z0-9]{3,8})?)/i.exec(flow.output)?.[1]
|
|
59
|
+
?? /\bcode\b[^\r\n]{0,100}\r?\n\s*([A-Z0-9]{3,8}(?:-[A-Z0-9]{3,8}){1,3})\b/i.exec(flow.output)?.[1];
|
|
60
|
+
const phase = flow.progress.agent === 'claude' ? 'awaiting-code' : 'awaiting-browser';
|
|
61
|
+
if (flow.progress.verificationURL === verificationURL && flow.progress.userCode === userCode && flow.progress.phase === phase) return;
|
|
62
|
+
flow.progress = { ...flow.progress, phase, verificationURL, userCode,
|
|
63
|
+
message: phase === 'awaiting-code' ? 'Open the provider, then paste its authorization code here.'
|
|
64
|
+
: 'Open the provider and approve sign-in. This computer will finish automatically.' };
|
|
65
|
+
flow.emit(flow.progress);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
code(scope: string, agent: CodingAgent, operationId: string, code: string): ProviderLoginProgress {
|
|
69
|
+
const flow = this.owned(scope, agent, operationId);
|
|
70
|
+
if (flow.progress.phase !== 'awaiting-code') throw Error('This sign-in is not waiting for an authorization code.');
|
|
71
|
+
if (/[\r\n\u0000]/.test(code) || !code.trim() || code.length > 4_096) throw Error('Paste a single authorization code.');
|
|
72
|
+
flow.child.stdin?.write(code.trim() + '\n');
|
|
73
|
+
return flow.progress;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cancel(scope: string, agent: CodingAgent, operationId: string): ProviderLoginProgress {
|
|
77
|
+
const flow = this.owned(scope, agent, operationId);
|
|
78
|
+
this.finish(operationId, 'cancelled', 'Sign-in cancelled. Existing credentials were not removed.');
|
|
79
|
+
return flow.progress;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private owned(scope: string, agent: CodingAgent, operationId: string): Flow {
|
|
83
|
+
const flow = this.flows.get(operationId);
|
|
84
|
+
if (!flow || flow.scope !== scope || flow.progress.agent !== agent) throw Error('This sign-in belongs to another controller or has expired.');
|
|
85
|
+
if (this.now() >= flow.progress.expiresAt) this.finish(operationId, 'expired', 'Sign-in expired. Start again.');
|
|
86
|
+
if (terminal.has(flow.progress.phase)) throw Error('This sign-in has ended.');
|
|
87
|
+
return flow;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private finish(id: string, phase: ProviderLoginProgress['phase'], message: string): void {
|
|
91
|
+
const flow = this.flows.get(id);
|
|
92
|
+
if (!flow || terminal.has(flow.progress.phase)) return;
|
|
93
|
+
clearTimeout(flow.timer);
|
|
94
|
+
if (phase !== 'signed-in') {
|
|
95
|
+
signalOwnedProcess(flow.child, 'SIGTERM');
|
|
96
|
+
const kill = setTimeout(() => {
|
|
97
|
+
if (flow.child.exitCode == null && flow.child.signalCode == null) signalOwnedProcess(flow.child, 'SIGKILL');
|
|
98
|
+
}, 1_000);
|
|
99
|
+
kill.unref();
|
|
100
|
+
}
|
|
101
|
+
flow.output = '';
|
|
102
|
+
flow.progress = { operationId: id, agent: flow.progress.agent, phase, message, expiresAt: flow.progress.expiresAt };
|
|
103
|
+
flow.emit(flow.progress);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private prune(): void {
|
|
107
|
+
for (const [id, f] of this.flows) if (terminal.has(f.progress.phase)) this.flows.delete(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export const providerLogins = new ProviderLogins();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir, hostname } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawn, execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import type { CodingAgent, DeviceManagementSnapshot } from '../../../../packages/protocol/schema';
|
|
9
|
+
import { configDir } from '../config/runtime/paths';
|
|
10
|
+
import { observedComputerId } from '../mesh/identity/computer';
|
|
11
|
+
import { automaticUpdatesAllowed } from '../runtime-updates/preferences/storage.mjs';
|
|
12
|
+
import { readState, runtimeVersion } from '../runtime-updates/selection.mjs';
|
|
13
|
+
import { checkForRuntimeUpdate } from '../runtime-updates';
|
|
14
|
+
import { binaryVersion } from '../tools/version';
|
|
15
|
+
import { providerCommand, providerAuthentication, installedProvider, providerEnvironment } from './provider-command';
|
|
16
|
+
import { computerCache } from './cache';
|
|
17
|
+
import { providerLogins } from './login';
|
|
18
|
+
import { ownedProviderProcesses, signalOwnedProcess } from '../reply/process';
|
|
19
|
+
import { approvalsStatus } from '../approvals/state';
|
|
20
|
+
|
|
21
|
+
export const runtimeRoot = fileURLToPath(new URL('../../../../', import.meta.url));
|
|
22
|
+
const runtimeEpoch = randomUUID();
|
|
23
|
+
const agents: CodingAgent[] = ['grok', 'claude', 'codex', 'cursor'];
|
|
24
|
+
|
|
25
|
+
export function runtimeRestartSupported(): boolean {
|
|
26
|
+
if (process.platform !== 'darwin' || !process.getuid || !runtimeRoot.includes('/node_modules/')) return false;
|
|
27
|
+
return ['com.granttap.monitor', 'com.granttap.mcp-http'].every(label => {
|
|
28
|
+
try {
|
|
29
|
+
const plist = readFileSync(join(homedir(), 'Library', 'LaunchAgents', label + '.plist'), 'utf8');
|
|
30
|
+
return plist.includes('<string>' + label + '</string>') && plist.includes('/granttap-mcp/bin/granttap-mcp.mjs')
|
|
31
|
+
&& plist.includes('<key>KeepAlive</key>');
|
|
32
|
+
} catch { return false; }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function maintenanceIdle(): boolean {
|
|
37
|
+
return !providerLogins.active() && approvalsStatus().pending.length === 0
|
|
38
|
+
&& agents.every(agent => ownedProviderProcesses(providerCommand(agent)).length === 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function managementSnapshot(): Promise<DeviceManagementSnapshot> {
|
|
42
|
+
const directory = join(configDir(), 'runtime-updates');
|
|
43
|
+
const selected = readState(directory);
|
|
44
|
+
const providers = await Promise.all(agents.map(async agent => {
|
|
45
|
+
const command = providerCommand(agent), installed = installedProvider(command);
|
|
46
|
+
return { agent, installed, version: binaryVersion(command), authentication: await providerAuthentication(agent, command),
|
|
47
|
+
loginSupported: installed, restartSupported: installed,
|
|
48
|
+
activeRequests: ownedProviderProcesses(command).length };
|
|
49
|
+
}));
|
|
50
|
+
return { computerId: observedComputerId(), computerName: hostname(), observedAt: Date.now(),
|
|
51
|
+
runtimeVersion: runtimeVersion(runtimeRoot), runtimeEpoch, selectedRuntimeVersion: selected.version,
|
|
52
|
+
automaticUpdatesEnabled: automaticUpdatesAllowed(directory),
|
|
53
|
+
runtimeUpdateSupported: runtimeRoot.includes('/node_modules/'), runtimeRestartSupported: runtimeRestartSupported(),
|
|
54
|
+
cache: (await computerCache(runtimeRoot)).map(({ paths: _paths, ...category }) => category), providers };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function updateRuntime(): Promise<string> {
|
|
58
|
+
if (!runtimeRoot.includes('/node_modules/')) throw Error('This is a development checkout. Update the installed MCP instead.');
|
|
59
|
+
const candidate = await checkForRuntimeUpdate({ root: runtimeRoot, stateDirectory: join(configDir(), 'runtime-updates'),
|
|
60
|
+
idle: maintenanceIdle, manual: true });
|
|
61
|
+
return candidate ? 'MCP update verified and selected. Restart MCP to activate it.'
|
|
62
|
+
: 'No newer published MCP is available, or activation is waiting for active work. The current MCP is retained.';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A detached, fixed command survives the monitor exiting. No phone-supplied command or path is executed. */
|
|
66
|
+
export function restartRuntime(): void {
|
|
67
|
+
if (!runtimeRestartSupported()) throw Error('Managed MCP services are not installed on this computer.');
|
|
68
|
+
if (!maintenanceIdle()) throw Error('Finish active requests, decisions and sign-in before restarting MCP.');
|
|
69
|
+
const domain = 'gui/' + process.getuid!();
|
|
70
|
+
const script = `setTimeout(() => { const { spawnSync } = require('node:child_process');
|
|
71
|
+
for (const label of ['com.granttap.mcp-http','com.granttap.monitor'])
|
|
72
|
+
spawnSync('/bin/launchctl',['kickstart','-k',${JSON.stringify(domain)}+'/'+label],{stdio:'ignore',timeout:10000}); }, 1200);`;
|
|
73
|
+
const child = spawn(process.execPath, ['-e', script], { detached: true, stdio: 'ignore' });
|
|
74
|
+
child.unref();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function restartProvider(agent: CodingAgent, interrupt = false): Promise<string> {
|
|
78
|
+
const command = providerCommand(agent);
|
|
79
|
+
if (!installedProvider(command)) throw Error('This provider is not installed on the computer.');
|
|
80
|
+
if (providerLogins.active()) throw Error('Finish sign-in before restarting a provider.');
|
|
81
|
+
const children = ownedProviderProcesses(command);
|
|
82
|
+
if (children.length && !interrupt) throw Error('This provider has active GrantTap requests. Confirm interruption before restarting.');
|
|
83
|
+
await Promise.all(children.map(child => new Promise<void>(resolve => {
|
|
84
|
+
const timer = setTimeout(() => { signalOwnedProcess(child, 'SIGKILL'); resolve(); }, 2_000);
|
|
85
|
+
child.once('close', () => { clearTimeout(timer); resolve(); });
|
|
86
|
+
(child as typeof child & { granttapStopped?: boolean }).granttapStopped = true;
|
|
87
|
+
signalOwnedProcess(child, 'SIGTERM');
|
|
88
|
+
})));
|
|
89
|
+
await promisify(execFile)(command, ['--version'], { env: providerEnvironment(), timeout: 15_000, maxBuffer: 16_384 });
|
|
90
|
+
return 'A fresh provider CLI started successfully. The next message starts a new GrantTap connection to it.';
|
|
91
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import type { CodingAgent } from '../../../../packages/protocol/schema';
|
|
6
|
+
import { executableOnPath, resolveClaudeBinary } from '../providers/claude-bin';
|
|
7
|
+
import { resolveCodexBinary } from '../providers/codex-bin';
|
|
8
|
+
import { resolveGrokBinary } from '../providers/grok-bin';
|
|
9
|
+
import { resolveCursorAgentBin } from '../reply/process/cursor-agent-bin';
|
|
10
|
+
|
|
11
|
+
export function providerCommand(agent: CodingAgent): string {
|
|
12
|
+
switch (agent) {
|
|
13
|
+
case 'claude': return resolveClaudeBinary().path;
|
|
14
|
+
case 'codex': return resolveCodexBinary();
|
|
15
|
+
case 'grok': return resolveGrokBinary();
|
|
16
|
+
case 'cursor': return resolveCursorAgentBin();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function providerEnvironment(): NodeJS.ProcessEnv {
|
|
21
|
+
const node = dirname(process.execPath);
|
|
22
|
+
return { ...process.env, PATH: [node, process.env.PATH ?? ''].join(delimiter),
|
|
23
|
+
NO_COLOR: '1', FORCE_COLOR: '0', DISABLE_AUTOUPDATER: '1', GROK_DISABLE_AUTOUPDATER: '1' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loginArguments(agent: CodingAgent): string[] {
|
|
27
|
+
if (agent === 'grok' || agent === 'codex') return ['login', '--device-auth'];
|
|
28
|
+
return agent === 'claude' ? ['auth', 'login'] : ['login'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Only known provider entry points are offered; loopback callbacks and token URLs are never relayed. */
|
|
32
|
+
export function allowedLoginURL(agent: CodingAgent, value: string): string | undefined {
|
|
33
|
+
try {
|
|
34
|
+
const u = new URL(value);
|
|
35
|
+
if (u.protocol !== 'https:' || u.username || u.password) return;
|
|
36
|
+
if ([...u.searchParams.keys()].some(k => /^(access_token|refresh_token|id_token|client_secret)$/i.test(k))) return;
|
|
37
|
+
const domains: Record<CodingAgent, string[]> = {
|
|
38
|
+
grok: ['grok.com', 'x.ai'], codex: ['openai.com', 'chatgpt.com'],
|
|
39
|
+
claude: ['claude.ai', 'claude.com', 'anthropic.com'], cursor: ['cursor.com', 'cursor.sh'],
|
|
40
|
+
};
|
|
41
|
+
if (domains[agent].some(domain => u.hostname === domain || u.hostname.endsWith('.' + domain))) return u.href;
|
|
42
|
+
if (agent === 'grok' && process.env.GROK_OIDC_ISSUER
|
|
43
|
+
&& new URL(process.env.GROK_OIDC_ISSUER).hostname === u.hostname) return u.href;
|
|
44
|
+
} catch { /* malformed or non-provider output */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function installedProvider(command: string): boolean {
|
|
48
|
+
try { accessSync(command.includes('/') ? command : executableOnPath(command, process.env) ?? command, constants.X_OK); return true; } catch { return false; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Authentication = 'signed-in' | 'signed-out' | 'unknown' | 'not-installed';
|
|
52
|
+
|
|
53
|
+
/** Presence is reported separately from a successful agent turn; credentials never leave their native store. */
|
|
54
|
+
export async function providerAuthentication(agent: CodingAgent, command = providerCommand(agent)): Promise<Authentication> {
|
|
55
|
+
if (!installedProvider(command)) return 'not-installed';
|
|
56
|
+
if (agent === 'grok') {
|
|
57
|
+
try {
|
|
58
|
+
const file = join(process.env.GRANTTAP_GROK_DIR ?? process.env.GROK_HOME ?? join(homedir(), '.grok'), 'auth.json');
|
|
59
|
+
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
60
|
+
return Object.values(data).some(value => value && typeof value === 'object'
|
|
61
|
+
&& typeof (value as { key?: unknown }).key === 'string') ? 'signed-in' : 'signed-out';
|
|
62
|
+
} catch { return process.env.XAI_API_KEY ? 'signed-in' : 'unknown'; }
|
|
63
|
+
}
|
|
64
|
+
const args = agent === 'claude' ? ['auth', 'status', '--json']
|
|
65
|
+
: agent === 'codex' ? ['login', 'status'] : ['status'];
|
|
66
|
+
return new Promise(resolve => {
|
|
67
|
+
let done = false, output = '';
|
|
68
|
+
const child = spawn(command, args, { env: providerEnvironment(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
69
|
+
const finish = (value: Authentication) => {
|
|
70
|
+
if (done) return; done = true; clearTimeout(timer); resolve(value);
|
|
71
|
+
};
|
|
72
|
+
const timer = setTimeout(() => { child.kill('SIGTERM'); finish('unknown'); }, 8_000);
|
|
73
|
+
child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8');
|
|
74
|
+
child.stdout.on('data', chunk => { output = (output + chunk).slice(-16_384); });
|
|
75
|
+
child.stderr.on('data', chunk => { output = (output + chunk).slice(-16_384); });
|
|
76
|
+
child.on('error', () => finish('unknown'));
|
|
77
|
+
child.on('close', code => {
|
|
78
|
+
if (agent === 'claude') {
|
|
79
|
+
try {
|
|
80
|
+
const value = JSON.parse(output);
|
|
81
|
+
if (typeof value.loggedIn === 'boolean') return finish(value.loggedIn ? 'signed-in' : 'signed-out');
|
|
82
|
+
} catch { /* an old CLI can only provide unknown status */ }
|
|
83
|
+
}
|
|
84
|
+
if (agent === 'codex') return finish(code === 0 ? 'signed-in' : /not logged|not signed/i.test(output) ? 'signed-out' : 'unknown');
|
|
85
|
+
if (/not logged|not signed|unauthenticated/i.test(output)) return finish('signed-out');
|
|
86
|
+
finish(code === 0 && /logged in|authenticated|signed in/i.test(output) ? 'signed-in' : 'unknown');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -29,7 +29,8 @@ Explicit Graph builds also invoke the Engine's `graph.analyze_project` for two
|
|
|
29
29
|
to eight admitted local repositories. The projection labels these shared static
|
|
30
30
|
contracts and keeps their runtime destination unverified. They do not become
|
|
31
31
|
verified Backbone dependencies or merge Project grants. The Apple Silicon
|
|
32
|
-
artifact pins Engine 0.1.
|
|
32
|
+
artifact pins Engine 0.1.5 with Weavatrix 2.17.5, injective package import identities
|
|
33
|
+
and per-file function extent reuse during metrics analysis; unsupported Engine operations
|
|
33
34
|
leave this analysis unavailable. See `runtime/project-relationships/README.md`.
|
|
34
35
|
|
|
35
36
|
Claude, Codex, and Cursor hooks can evaluate the engine's local Project
|
|
@@ -8,8 +8,8 @@ export type EngineArtifact = {
|
|
|
8
8
|
|
|
9
9
|
/** Release hashes are part of the reviewed MCP, never supplied by a remote manifest. */
|
|
10
10
|
export const ENGINE_ARTIFACTS: readonly EngineArtifact[] = [
|
|
11
|
-
{ version: "0.1.
|
|
12
|
-
sha256: "
|
|
11
|
+
{ version: "0.1.5", platform: "darwin", arch: "arm64",
|
|
12
|
+
sha256: "bd16babf11fd3d7e65b9713797b977deba1670ba41230a0e6607ce938b847455", bytes: 12202080 },
|
|
13
13
|
{ version: "0.1.2", platform: "darwin", arch: "x64",
|
|
14
14
|
sha256: "b3235a15936a77ec323d5ecbc54a0bc19994fe6e5a36044da964b68524b4c1dd", bytes: 13944984 },
|
|
15
15
|
{ version: "0.1.2", platform: "linux", arch: "x64",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import type { ProjectRestrictionSet } from "../../../../../packages/protocol/schema";
|
|
4
|
+
import { hasGuidance } from "./guidance";
|
|
4
5
|
import { evaluateContent, type RestrictionViolation } from "./evaluate";
|
|
5
6
|
|
|
6
7
|
const SKIP_DIR = new Set([
|
|
@@ -62,12 +63,12 @@ function isTextish(name: string): boolean {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export function restrictionSummary(restrictions: ProjectRestrictionSet | undefined): string {
|
|
65
|
-
if (!restrictions || restrictions.rules.length === 0) return "No restrictions";
|
|
66
|
+
if (!restrictions || (restrictions.rules.length === 0 && !hasGuidance(restrictions.guidance))) return "No restrictions";
|
|
66
67
|
const count = restrictions.rules.length;
|
|
67
68
|
const scope = restrictions.scope === "sync_from_repo"
|
|
68
69
|
? "from the repository"
|
|
69
70
|
: restrictions.scope === "project_and_repo"
|
|
70
71
|
? "Project and repository"
|
|
71
72
|
: "this Project";
|
|
72
|
-
return `${count} rule${count === 1 ? "" : "s"} · ${scope}`;
|
|
73
|
+
return `${count} rule${count === 1 ? "" : "s"}${hasGuidance(restrictions.guidance) ? " + agent guidance" : ""} · ${scope}`;
|
|
73
74
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasGuidance } from "./guidance";
|
|
1
2
|
import { inspectRepository } from "../catalog";
|
|
2
3
|
import { computerId } from "../identity/computer";
|
|
3
4
|
import { localMeshStore } from "../local-remote/local";
|
|
@@ -19,7 +20,10 @@ export function runRestrictionsCheck(root: string): RestrictionsCheckResult {
|
|
|
19
20
|
const restrictions = readRepoRestrictions(root, projectId ?? "ci")
|
|
20
21
|
?? (projectId ? loadRestrictions(projectId) : undefined);
|
|
21
22
|
|
|
23
|
+
const advisory = hasGuidance(restrictions?.guidance)
|
|
24
|
+
? "Agent guidance is advisory and not machine-checked by this command.\n" : "";
|
|
22
25
|
if (!restrictions || restrictions.rules.length === 0) {
|
|
26
|
+
if (advisory) return { exitCode: 0, stdout: advisory, stderr: "" };
|
|
23
27
|
return { exitCode: 0, stdout: "GrantTap restrictions: none configured.\n", stderr: "" };
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -27,7 +31,7 @@ export function runRestrictionsCheck(root: string): RestrictionsCheckResult {
|
|
|
27
31
|
if (violations.length === 0) {
|
|
28
32
|
return {
|
|
29
33
|
exitCode: 0,
|
|
30
|
-
stdout: `GrantTap restrictions: ${restrictions.rules.length} rule(s) clean.\n`,
|
|
34
|
+
stdout: `GrantTap restrictions: ${restrictions.rules.length} rule(s) clean.\n${advisory}`,
|
|
31
35
|
stderr: "",
|
|
32
36
|
};
|
|
33
37
|
}
|
|
@@ -36,5 +40,5 @@ export function runRestrictionsCheck(root: string): RestrictionsCheckResult {
|
|
|
36
40
|
.map((item) => `${item.effect.toUpperCase()} ${item.reason}\n`)
|
|
37
41
|
.join("");
|
|
38
42
|
const denied = violations.some((item) => item.effect === "deny");
|
|
39
|
-
return { exitCode: denied ? 1 : 0, stdout:
|
|
43
|
+
return { exitCode: denied ? 1 : 0, stdout: advisory, stderr };
|
|
40
44
|
}
|
|
@@ -38,7 +38,7 @@ export function functionLineCounts(source: string): number[] {
|
|
|
38
38
|
|
|
39
39
|
function unparsedFunctionForm(filePath: string, content: string): boolean {
|
|
40
40
|
const extension = filePath.split(".").at(-1)?.toLowerCase();
|
|
41
|
-
if (extension
|
|
41
|
+
if (["py", "rb"].includes(extension ?? "")) return /^\s*(?:async\s+)?def\s+\w+/m.test(content);
|
|
42
42
|
if (!["ts", "tsx", "js", "jsx", "mjs", "cjs"].includes(extension ?? "")) return false;
|
|
43
43
|
if (/=>/.test(content)) return true;
|
|
44
44
|
return /^\s*(?:async\s+)?(?!(?:if|while|for|switch|catch)\b)[A-Za-z_$][\w$]*\s*\([^;\n]*\)\s*\{/m
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Project coding guidance
|
|
2
|
+
|
|
3
|
+
`projectGuidancePrompt(projectId, text)` is the public prompt boundary. It loads
|
|
4
|
+
only the accepted local restriction set for the named Project, adds its
|
|
5
|
+
advisory principles, architecture preference, design targets and exact Markdown,
|
|
6
|
+
then preserves the original request. Empty guidance is a no-op. It never edits
|
|
7
|
+
provider configuration or raises tool permissions.
|
|
8
|
+
|
|
9
|
+
Continuation delivery calls this through the existing routing prompt for all
|
|
10
|
+
supported providers. New phone Tasks use the Project returned by validated
|
|
11
|
+
workspace admission. Transfer starts use the existing Task's Project scope.
|
|
12
|
+
A loose cwd or unvalidated message Project ID does not supply instruction scope.
|
|
13
|
+
Original delivery identities, origin records and run journals retain the person's
|
|
14
|
+
original request; attached guidance is not a separately delivered prompt.
|
|
15
|
+
|
|
16
|
+
The optional protocol field is backward compatible for reading old sets. A Mac
|
|
17
|
+
that predates this contract must be upgraded before a phone sends guidance;
|
|
18
|
+
strict old protocol receivers cannot honestly acknowledge a field they reject.
|
|
19
|
+
The canonical policy receipt proves local acceptance, not semantic compliance.
|
|
20
|
+
|
|
21
|
+
The private store and `.granttap/restrictions.json` export preserve all guidance.
|
|
22
|
+
The explicit repository check reports guidance as advisory and never claims to
|
|
23
|
+
machine-check Clean Code, architecture, class or parameter targets. Existing
|
|
24
|
+
write checks own numeric enforcement; Ruby/Python function forms outside the
|
|
25
|
+
brace scanner request parser review.
|
|
26
|
+
|
|
27
|
+
License: GrantTap Commercial Source License, in the repository-root `LICENSE`.
|