@kontourai/flow-agents 0.1.2 → 0.2.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/.github/dependabot.yml +23 -0
- package/.github/workflows/release-please.yml +31 -0
- package/.github/workflows/runtime-compat.yml +118 -0
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +53 -10
- package/build/src/cli/init.js +215 -5
- package/build/src/cli/utterance-check.js +65 -1
- package/build/src/tools/build-universal-bundles.js +268 -0
- package/build/src/tools/filter-installed-packs.js +3 -0
- package/build/src/tools/validate-source-tree.js +5 -1
- package/context/scripts/telemetry/lib/config.sh +5 -1
- package/context/settings/flow-agents-settings.json +7 -0
- package/docs/context-map.md +1 -0
- package/docs/index.md +45 -4
- package/docs/integrations/conformance.md +246 -0
- package/docs/integrations/framework-adapter.md +275 -0
- package/docs/integrations/harness-install.md +213 -0
- package/docs/integrations/index.md +54 -0
- package/docs/north-star.md +2 -2
- package/docs/spec/runtime-hook-surface.md +472 -0
- package/docs/survey-utterance-check.md +211 -94
- package/docs/vision.md +45 -0
- package/evals/acceptance/run.sh +4 -2
- package/evals/acceptance/test_opencode_harness.sh +121 -0
- package/evals/acceptance/test_pi_harness.sh +98 -0
- package/evals/integration/test_bundle_install.sh +226 -1
- package/evals/integration/test_bundle_lifecycle.sh +641 -0
- package/evals/integration/test_utterance_check.sh +291 -44
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +137 -2
- package/integrations/strands/README.md +256 -0
- package/integrations/strands/example.py +74 -0
- package/integrations/strands/flow_agents_strands/__init__.py +27 -0
- package/integrations/strands/flow_agents_strands/hooks.py +194 -0
- package/integrations/strands/flow_agents_strands/policy.py +348 -0
- package/integrations/strands/flow_agents_strands/steering.py +172 -0
- package/integrations/strands/flow_agents_strands/telemetry.py +238 -0
- package/integrations/strands/pyproject.toml +38 -0
- package/integrations/strands/tests/__init__.py +0 -0
- package/integrations/strands/tests/test_hooks.py +304 -0
- package/integrations/strands/tests/test_policy.py +315 -0
- package/integrations/strands/tests/test_telemetry.py +184 -0
- package/integrations/strands-ts/README.md +224 -0
- package/integrations/strands-ts/bin/conformance-shim.mjs +257 -0
- package/integrations/strands-ts/package.json +53 -0
- package/integrations/strands-ts/src/hooks.ts +208 -0
- package/integrations/strands-ts/src/index.ts +22 -0
- package/integrations/strands-ts/src/policy.ts +345 -0
- package/integrations/strands-ts/src/telemetry.ts +251 -0
- package/integrations/strands-ts/test/test-policy.ts +322 -0
- package/integrations/strands-ts/test/test-telemetry.ts +226 -0
- package/integrations/strands-ts/tsconfig.json +20 -0
- package/package.json +7 -2
- package/packaging/conformance/README.md +142 -0
- package/packaging/conformance/fixtures/config-protection--allow-no-path.json +18 -0
- package/packaging/conformance/fixtures/config-protection--allow-safe-file.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-biome.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-eslintrc.json +20 -0
- package/packaging/conformance/fixtures/quality-gate--allow-no-path.json +17 -0
- package/packaging/conformance/fixtures/quality-gate--allow-nonexistent-file.json +19 -0
- package/packaging/conformance/fixtures/stop-goal-fit--allow-clean-cwd.json +17 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-strict-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +21 -0
- package/packaging/conformance/fixtures/workflow-steering--allow-no-state.json +16 -0
- package/packaging/conformance/fixtures/workflow-steering--inject-active-state.json +29 -0
- package/packaging/conformance/fixtures/workflow-steering--inject-subagent-steering.json +25 -0
- package/packaging/conformance/package.json +4 -0
- package/packaging/conformance/run-conformance.js +322 -0
- package/packaging/manifest.json +59 -0
- package/schemas/flow-agents-settings.schema.json +48 -0
- package/scripts/README.md +4 -0
- package/scripts/dogfood.js +16 -0
- package/scripts/hooks/opencode-hook-adapter.js +123 -0
- package/scripts/hooks/opencode-telemetry-hook.js +101 -0
- package/scripts/hooks/pi-hook-adapter.js +123 -0
- package/scripts/hooks/pi-telemetry-hook.js +105 -0
- package/scripts/hooks/run-hook.js +8 -0
- package/scripts/hooks/utterance-check.js +124 -22
- package/scripts/telemetry/lib/config.sh +5 -1
- package/src/cli/init.ts +219 -6
- package/src/cli/utterance-check.ts +71 -1
- package/src/tools/build-universal-bundles.ts +266 -0
- package/src/tools/filter-installed-packs.ts +3 -0
- package/src/tools/validate-source-tree.ts +5 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# @kontourai/flow-agents-strands
|
|
2
|
+
|
|
3
|
+
**Native-import TypeScript adapter for AWS Strands Agents.**
|
|
4
|
+
|
|
5
|
+
This is the first native-import consumer of the Flow Agents policy engine contract. It wires Flow Agents telemetry, workflow steering, and policy gates directly into Strands Agents TypeScript SDK hook callbacks — with no subprocess overhead for the critical hot path (config-protection on `BeforeToolCallEvent`).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Native-import vs subprocess binding
|
|
10
|
+
|
|
11
|
+
| Aspect | This adapter (TS, native) | Python adapter (subprocess) |
|
|
12
|
+
|--------|---------------------------|-----------------------------|
|
|
13
|
+
| Language | TypeScript / Node.js | Python |
|
|
14
|
+
| Engine binding | `require("config-protection.js")` — in-process | `subprocess.run(["node", "run-hook.js", …])` |
|
|
15
|
+
| Hot path latency | ~0 ms (direct function call) | ~50–100 ms per call (process spawn) |
|
|
16
|
+
| Strands SDK optional? | Yes — duck-typed, SDK not required to build/test | Yes |
|
|
17
|
+
| Config-protection | Native `run()` call | Subprocess, with Python fallback |
|
|
18
|
+
| Other policies (steering, quality-gate, stop-goal-fit) | Via shim subprocess (conformance runner) | Via subprocess |
|
|
19
|
+
| Conformance level | L2 | L0 (+ config-protection) |
|
|
20
|
+
|
|
21
|
+
The key innovation: `config-protection.js` exports `module.exports = { run }`. This adapter calls that function directly from the Node.js process, bypassing the subprocess round-trip for every `BeforeToolCallEvent` write call.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { Agent, BeforeInvocationEvent, AfterInvocationEvent,
|
|
29
|
+
BeforeToolCallEvent, AfterToolCallEvent } from "@strands-agents/sdk";
|
|
30
|
+
import { FlowAgentsHooks } from "@kontourai/flow-agents-strands";
|
|
31
|
+
|
|
32
|
+
// Construct — no strands-agents import needed at this point
|
|
33
|
+
const hooks = new FlowAgentsHooks({
|
|
34
|
+
workspace: ".", // reads .telemetry/ for JSONL output
|
|
35
|
+
agentName: "my-agent", // embedded in telemetry events
|
|
36
|
+
// engineRoot: "/path/to/flow-agents" // optional: explicit engine location
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Wire into the agent
|
|
40
|
+
const agent = new Agent({ hooks: [hooks] });
|
|
41
|
+
|
|
42
|
+
// Optionally emit agentSpawn telemetry immediately
|
|
43
|
+
hooks.emitSessionStart();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or wire manually without the SDK:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Direct callback wiring (for testing or custom frameworks)
|
|
50
|
+
hooks.onBeforeInvocation(event);
|
|
51
|
+
hooks.onBeforeToolCall({ toolName: "write", toolInput: { path: "biome.json" } });
|
|
52
|
+
// → event.cancel is set to block reason if config-protection fires
|
|
53
|
+
hooks.onAfterToolCall({ toolName: "write", result: "ok" });
|
|
54
|
+
hooks.onAfterInvocation(event);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Event mapping
|
|
60
|
+
|
|
61
|
+
The Strands-TS → canonical mapping is exported as `STRANDS_TO_CANONICAL`:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { STRANDS_TO_CANONICAL } from "@kontourai/flow-agents-strands";
|
|
65
|
+
// {
|
|
66
|
+
// BeforeInvocationEvent: "userPromptSubmit",
|
|
67
|
+
// AfterInvocationEvent: "stop",
|
|
68
|
+
// BeforeToolCallEvent: "preToolUse",
|
|
69
|
+
// AfterToolCallEvent: "postToolUse",
|
|
70
|
+
// AgentInitializedEvent: "agentSpawn",
|
|
71
|
+
// AfterModelCallEvent: "postToolUse",
|
|
72
|
+
// MessageAddedEvent: "userPromptSubmit",
|
|
73
|
+
// }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Strands TS Event | Canonical event | JSONL event_type |
|
|
77
|
+
|------------------|-----------------|-----------------|
|
|
78
|
+
| `BeforeInvocationEvent` | `userPromptSubmit` | `turn.user` |
|
|
79
|
+
| `AfterInvocationEvent` | `stop` | `session.end` |
|
|
80
|
+
| `BeforeToolCallEvent` | `preToolUse` | `tool.invoke` |
|
|
81
|
+
| `AfterToolCallEvent` | `postToolUse` | `tool.result` |
|
|
82
|
+
| `AgentInitializedEvent` | `agentSpawn` | `session.start` |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Telemetry
|
|
87
|
+
|
|
88
|
+
Events are written to `<workspace>/.telemetry/full.jsonl` (matching the canonical config.sh path `TELEMETRY_DATA_DIR/.../full.jsonl`).
|
|
89
|
+
|
|
90
|
+
Event shape matches `build_base_event()` in `scripts/telemetry/telemetry.sh` at schema version `0.3.0`:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"schema_version": "0.3.0",
|
|
95
|
+
"timestamp": "1718000000000",
|
|
96
|
+
"session_id": "<uuid>",
|
|
97
|
+
"event_id": "<uuid>",
|
|
98
|
+
"event_type": "tool.invoke",
|
|
99
|
+
"agent": { "name": "my-agent", "runtime": "strands-ts", "version": "unknown" },
|
|
100
|
+
"hook": {
|
|
101
|
+
"event_name": "preToolUse",
|
|
102
|
+
"source": "strands-ts",
|
|
103
|
+
"stop_hook_active": null,
|
|
104
|
+
"raw_input": null
|
|
105
|
+
},
|
|
106
|
+
"tool": { "name": "edit", "normalized_name": "fs_write", "input": { ... } }
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Telemetry is always fail-open: write errors are silently swallowed so telemetry never blocks agent work.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Config-protection policy gate
|
|
115
|
+
|
|
116
|
+
On `BeforeToolCallEvent`, for write-like tools (`edit`, `write`, `fs_write`, `apply_patch`, `create_file`, `str_replace_editor`), the gate calls the native engine:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// Under the hood — native import, no subprocess:
|
|
120
|
+
const { run } = require("scripts/hooks/config-protection.js");
|
|
121
|
+
const result = run(jsonPayload, { truncated: false, maxStdin: 1024 * 1024 });
|
|
122
|
+
// result.exitCode === 2 → set event.cancel = result.stderr
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If blocked, `event.cancel` is set to the block reason. Strands cancels the tool call and surfaces the message as the tool result.
|
|
126
|
+
|
|
127
|
+
**Engine auto-discovery** (in priority order):
|
|
128
|
+
1. `FlowAgentsHooksOptions.engineRoot` (explicit constructor option)
|
|
129
|
+
2. `FLOW_AGENTS_ENGINE_ROOT` env var
|
|
130
|
+
3. Relative to this package (works from repo checkout)
|
|
131
|
+
4. Walk up from `process.cwd()` for `node_modules/@kontourai/flow-agents/`
|
|
132
|
+
|
|
133
|
+
**Fallback**: if the engine cannot be loaded, a one-time `console.warn` is emitted and the built-in TypeScript implementation (same protected-files list) is used. Fail-open for all errors.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Conformance
|
|
138
|
+
|
|
139
|
+
Tested against the Flow Agents conformance kit (`packaging/conformance/`):
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
conformance_level: L2
|
|
143
|
+
engine_contract_version: "1.0"
|
|
144
|
+
runner_version: "run-conformance.js"
|
|
145
|
+
test_date: 2026-06-11
|
|
146
|
+
verdict: PASS
|
|
147
|
+
fixture_count: 12
|
|
148
|
+
fixtures_passed: 12
|
|
149
|
+
gaps: []
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Run the conformance test from the repo root:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
node packaging/conformance/run-conformance.js \
|
|
156
|
+
--adapter-cmd "node integrations/strands-ts/bin/conformance-shim.mjs" \
|
|
157
|
+
--level L2
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Running tests
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# From repo root (no npm install needed — uses root node_modules/typescript):
|
|
166
|
+
npx tsc -p integrations/strands-ts/tsconfig.json
|
|
167
|
+
node --test integrations/strands-ts/dist/test/test-telemetry.js \
|
|
168
|
+
integrations/strands-ts/dist/test/test-policy.js
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
47 tests, no strands-agents required.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Limitations
|
|
176
|
+
|
|
177
|
+
1. **No per-turn workflow steering injection**: Strands' `BeforeInvocationEvent` does not expose a mutable system prompt. Unlike the harness adapters which inject workflow state at each `UserPromptSubmit`, this adapter emits the telemetry event only. Productization requires upstream SDK support or a custom model wrapper.
|
|
178
|
+
|
|
179
|
+
2. **Quality-gate and stop-goal-fit via subprocess in conformance shim only**: The production `FlowAgentsHooks` callbacks don't wire `quality-gate.js` or `stop-goal-fit.js` (they have no clear Strands analogue for direct callback injection). The `bin/conformance-shim.mjs` shim wires them via subprocess for conformance certification only.
|
|
180
|
+
|
|
181
|
+
3. **session.usage event omitted**: The `AfterInvocationEvent` does not expose token usage in the Strands TS SDK hook payload.
|
|
182
|
+
|
|
183
|
+
4. **No analytics channel**: Only the `full` JSONL channel is written. Analytics-channel redaction is not implemented.
|
|
184
|
+
|
|
185
|
+
5. **No Console/HTTP sink**: JSONL file output only. HTTP transport would require implementing the `console_telemetry_emit()` logic.
|
|
186
|
+
|
|
187
|
+
6. **Runtime version is "unknown"**: The Strands TS SDK does not expose its version through hook event payloads.
|
|
188
|
+
|
|
189
|
+
7. **No subagent/delegation events**: The Strands TS SDK has no built-in `InvokeSubagents` tool; `subagentStart`/`subagentStop` telemetry paths are not wired.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Conformance declaration
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
conformance_level: L2 (via conformance-shim.mjs)
|
|
197
|
+
host: AWS Strands Agents TypeScript SDK
|
|
198
|
+
event_coverage:
|
|
199
|
+
agentSpawn: emitSessionStart() — full fidelity
|
|
200
|
+
userPromptSubmit: BeforeInvocationEvent — telemetry only, no per-turn injection
|
|
201
|
+
preToolUse: BeforeToolCallEvent — full fidelity, blocking via event.cancel
|
|
202
|
+
postToolUse: AfterToolCallEvent — telemetry only; quality-gate via shim
|
|
203
|
+
stop: AfterInvocationEvent — telemetry only; stop-goal-fit via shim
|
|
204
|
+
permissionRequest: no native equivalent
|
|
205
|
+
subagentStart: no native equivalent
|
|
206
|
+
subagentStop: no native equivalent
|
|
207
|
+
policy_coverage:
|
|
208
|
+
config_protection: wired at BeforeToolCallEvent (native import, blocking)
|
|
209
|
+
workflow_steering: telemetry-only at BeforeInvocationEvent; shim wires for conformance
|
|
210
|
+
quality_gate: shim only (no direct Strands callback equivalent)
|
|
211
|
+
stop_goal_fit: shim only (no direct Strands callback equivalent)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Live validation status
|
|
215
|
+
|
|
216
|
+
The canonical-taxonomy live loop is proven end-to-end with no API keys via the
|
|
217
|
+
Python adapter (`integrations/strands/`): a real Strands agent on a local
|
|
218
|
+
Ollama model (qwen3:1.7b) with `FlowAgentsHooks` attached persisted all five
|
|
219
|
+
canonical event types (`session.start`, `turn.user`, `tool.invoke`,
|
|
220
|
+
`tool.result`, `session.end`) on 2026-06-11. The TypeScript SDK currently
|
|
221
|
+
ships only a Bedrock model provider, so this adapter's live-agent run requires
|
|
222
|
+
AWS credentials; its correctness is covered by the real-engine tests and the
|
|
223
|
+
L2 conformance certification above. An Ollama `Model` implementation for the
|
|
224
|
+
TS SDK is a candidate follow-up if keyless live runs are wanted here too.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* conformance-shim.mjs — Adapter shim for run-conformance.js --adapter-cmd.
|
|
4
|
+
*
|
|
5
|
+
* Receives a canonical JSON payload on stdin (one JSON object, the fixture payload).
|
|
6
|
+
* Invokes the Flow Agents policy engine via native import for preToolUse,
|
|
7
|
+
* and via subprocess for other hook types — matching the adapter's production behavior.
|
|
8
|
+
*
|
|
9
|
+
* Exits 0 (allow) or 2 (block) per the engine contract §8.3.
|
|
10
|
+
*
|
|
11
|
+
* Routing table (matches HOOK_MAP in production adapter):
|
|
12
|
+
* PreToolUse → config-protection.js (NATIVE import — no subprocess)
|
|
13
|
+
* PostToolUse → quality-gate.js + workflow-steering.js (both via subprocess)
|
|
14
|
+
* UserPromptSubmit → workflow-steering.js
|
|
15
|
+
* Stop → stop-goal-fit.js
|
|
16
|
+
*
|
|
17
|
+
* For PostToolUse, workflow-steering.js is checked first (for InvokeSubagents).
|
|
18
|
+
* If it injects content (non-empty injection), that's included in stdout.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
// Resolve repo root: walk up from __dirname looking for scripts/hooks/run-hook.js
|
|
33
|
+
function findRepoRoot(start) {
|
|
34
|
+
let current = start;
|
|
35
|
+
for (let i = 0; i < 10; i++) {
|
|
36
|
+
const candidate = path.join(current, 'scripts', 'hooks', 'run-hook.js');
|
|
37
|
+
if (fs.existsSync(candidate)) return current;
|
|
38
|
+
const parent = path.dirname(current);
|
|
39
|
+
if (parent === current) return null;
|
|
40
|
+
current = parent;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const repoRoot = findRepoRoot(__dirname);
|
|
46
|
+
const hooksDir = repoRoot ? path.join(repoRoot, 'scripts', 'hooks') : null;
|
|
47
|
+
const runHookPath = hooksDir ? path.join(hooksDir, 'run-hook.js') : null;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Read stdin
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const MAX_STDIN = 1024 * 1024;
|
|
54
|
+
|
|
55
|
+
async function readStdin() {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
let raw = '';
|
|
58
|
+
let truncated = false;
|
|
59
|
+
process.stdin.setEncoding('utf8');
|
|
60
|
+
process.stdin.on('data', (chunk) => {
|
|
61
|
+
if (raw.length < MAX_STDIN) {
|
|
62
|
+
const remaining = MAX_STDIN - raw.length;
|
|
63
|
+
raw += chunk.substring(0, remaining);
|
|
64
|
+
if (chunk.length > remaining) truncated = true;
|
|
65
|
+
} else {
|
|
66
|
+
truncated = true;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
process.stdin.on('end', () => resolve({ raw, truncated }));
|
|
70
|
+
process.stdin.on('error', () => resolve({ raw, truncated }));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Invoke a hook script via subprocess (for non-blocking hooks)
|
|
76
|
+
// Returns the subprocess result object
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
function invokeViaSubprocess(hookId, hookScript, raw) {
|
|
80
|
+
if (!runHookPath || !fs.existsSync(runHookPath)) {
|
|
81
|
+
return { status: 0, stdout: raw, stderr: '' };
|
|
82
|
+
}
|
|
83
|
+
const result = spawnSync(
|
|
84
|
+
process.execPath,
|
|
85
|
+
[runHookPath, hookId, hookScript],
|
|
86
|
+
{
|
|
87
|
+
input: raw,
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
env: { ...process.env, FLOW_AGENTS_HOOK_RUNTIME: 'strands-ts' },
|
|
90
|
+
timeout: 15000,
|
|
91
|
+
cwd: repoRoot,
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Main
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const { raw, truncated } = await readStdin();
|
|
103
|
+
|
|
104
|
+
let payload;
|
|
105
|
+
try {
|
|
106
|
+
payload = JSON.parse(raw);
|
|
107
|
+
} catch {
|
|
108
|
+
process.stdout.write(raw);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const hookEventName = (payload.hook_event_name || '').toLowerCase();
|
|
113
|
+
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// PreToolUse → config-protection.js via NATIVE import (no subprocess)
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
if (hookEventName === 'pretooluse') {
|
|
119
|
+
if (!hooksDir) {
|
|
120
|
+
process.stdout.write(raw);
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const require = createRequire(import.meta.url);
|
|
125
|
+
const configProtectionPath = path.join(hooksDir, 'config-protection.js');
|
|
126
|
+
|
|
127
|
+
if (!fs.existsSync(configProtectionPath)) {
|
|
128
|
+
process.stdout.write(raw);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let mod;
|
|
133
|
+
try {
|
|
134
|
+
mod = require(configProtectionPath);
|
|
135
|
+
} catch {
|
|
136
|
+
process.stdout.write(raw);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (mod && typeof mod.run === 'function') {
|
|
141
|
+
let result;
|
|
142
|
+
try {
|
|
143
|
+
result = mod.run(raw, { truncated, maxStdin: MAX_STDIN });
|
|
144
|
+
} catch {
|
|
145
|
+
process.stdout.write(raw);
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof result === 'string') {
|
|
150
|
+
process.stdout.write(result);
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
if (result && typeof result === 'object') {
|
|
154
|
+
if (result.stderr) {
|
|
155
|
+
process.stderr.write(result.stderr.endsWith('\n') ? result.stderr : result.stderr + '\n');
|
|
156
|
+
}
|
|
157
|
+
const exitCode = typeof result.exitCode === 'number' ? result.exitCode : 0;
|
|
158
|
+
if (exitCode === 2) {
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
if (Object.prototype.hasOwnProperty.call(result, 'stdout')) {
|
|
162
|
+
process.stdout.write(String(result.stdout ?? ''));
|
|
163
|
+
} else {
|
|
164
|
+
process.stdout.write(raw);
|
|
165
|
+
}
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.stdout.write(raw);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// -------------------------------------------------------------------------
|
|
175
|
+
// PostToolUse → run workflow-steering.js first (handles InvokeSubagents),
|
|
176
|
+
// then quality-gate.js
|
|
177
|
+
//
|
|
178
|
+
// The conformance runner checks stdout_contains for the workflow-steering
|
|
179
|
+
// fixture so we must return the output of workflow-steering when it fires.
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
if (hookEventName === 'posttooluse') {
|
|
183
|
+
if (!runHookPath) {
|
|
184
|
+
process.stdout.write(raw);
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Run workflow-steering first — may inject EXECUTION COMPLETE for subagent calls
|
|
189
|
+
const steeringResult = invokeViaSubprocess('workflow-steering', 'workflow-steering.js', raw);
|
|
190
|
+
const steeringOut = steeringResult.stdout || '';
|
|
191
|
+
if (steeringResult.stderr) process.stderr.write(steeringResult.stderr);
|
|
192
|
+
|
|
193
|
+
// Then run quality-gate (always non-blocking, exit 0)
|
|
194
|
+
// Use whatever output workflow-steering produced as input for quality-gate
|
|
195
|
+
// so both hooks chain correctly.
|
|
196
|
+
const steeringChainInput = steeringOut || raw;
|
|
197
|
+
const qualityResult = invokeViaSubprocess('quality-gate', 'quality-gate.js', steeringChainInput);
|
|
198
|
+
const qualityOut = qualityResult.stdout || '';
|
|
199
|
+
if (qualityResult.stderr) process.stderr.write(qualityResult.stderr);
|
|
200
|
+
|
|
201
|
+
// Return the final output (quality-gate output, which includes steering output)
|
|
202
|
+
process.stdout.write(qualityOut || steeringOut || raw);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// -------------------------------------------------------------------------
|
|
207
|
+
// UserPromptSubmit → workflow-steering.js
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
if (hookEventName === 'userpromptsubmit') {
|
|
211
|
+
if (!runHookPath) {
|
|
212
|
+
process.stdout.write(raw);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const result = invokeViaSubprocess('workflow-steering', 'workflow-steering.js', raw);
|
|
217
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
218
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
219
|
+
if (!result.stdout) process.stdout.write(raw);
|
|
220
|
+
|
|
221
|
+
if (result.error || result.signal || result.status === null) {
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
process.exit(typeof result.status === 'number' ? result.status : 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// -------------------------------------------------------------------------
|
|
228
|
+
// Stop → stop-goal-fit.js
|
|
229
|
+
// -------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
if (hookEventName === 'stop') {
|
|
232
|
+
if (!runHookPath) {
|
|
233
|
+
process.stdout.write(raw);
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const result = invokeViaSubprocess('stop-goal-fit', 'stop-goal-fit.js', raw);
|
|
238
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
239
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
240
|
+
if (!result.stdout) process.stdout.write(raw);
|
|
241
|
+
|
|
242
|
+
if (result.error || result.signal || result.status === null) {
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
process.exit(typeof result.status === 'number' ? result.status : 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Unknown hook event — pass through
|
|
249
|
+
process.stdout.write(raw);
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main().catch((err) => {
|
|
254
|
+
process.stderr.write(`[conformance-shim] error: ${err.message}\n`);
|
|
255
|
+
process.stdout.write('{}');
|
|
256
|
+
process.exit(0);
|
|
257
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kontourai/flow-agents-strands",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Native-import TypeScript adapter for AWS Strands Agents — wires Flow Agents policy engine directly into Strands hook callbacks without spawning a subprocess.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"ai-agents",
|
|
8
|
+
"strands",
|
|
9
|
+
"flow-agents",
|
|
10
|
+
"hooks",
|
|
11
|
+
"telemetry",
|
|
12
|
+
"policy"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/kontourai/flow-agents.git",
|
|
19
|
+
"directory": "integrations/strands-ts"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./dist/src/index.js",
|
|
27
|
+
"types": "./dist/src/index.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/src/index.js",
|
|
31
|
+
"types": "./dist/src/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/",
|
|
34
|
+
"bin/",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "npx tsc -p tsconfig.json",
|
|
39
|
+
"typecheck": "npx tsc -p tsconfig.json --noEmit",
|
|
40
|
+
"test": "npm run build && node --test dist/test/test-*.js",
|
|
41
|
+
"conformance:self": "node ../../packaging/conformance/run-conformance.js --self",
|
|
42
|
+
"conformance": "node ../../packaging/conformance/run-conformance.js --adapter-cmd \"node bin/conformance-shim.mjs\" --level L2"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@strands-agents/sdk": ">=1.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@strands-agents/sdk": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {}
|
|
53
|
+
}
|