scene-capability-engine 3.6.2 → 3.6.4
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/CHANGELOG.md +54 -0
- package/README.md +15 -2
- package/README.zh.md +15 -2
- package/bin/scene-capability-engine.js +33 -1
- package/docs/command-reference.md +87 -0
- package/lib/collab/agent-registry.js +38 -1
- package/lib/commands/auth.js +269 -0
- package/lib/commands/session.js +60 -2
- package/lib/commands/state.js +210 -0
- package/lib/commands/studio.js +57 -7
- package/lib/commands/task.js +25 -2
- package/lib/runtime/project-timeline.js +202 -17
- package/lib/runtime/session-store.js +167 -14
- package/lib/security/write-authorization.js +632 -0
- package/lib/state/sce-state-store.js +1029 -1
- package/lib/state/state-migration-manager.js +659 -0
- package/lib/steering/compliance-error-reporter.js +6 -0
- package/lib/steering/steering-compliance-checker.js +43 -8
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.6.4] - 2026-03-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Gradual state migration command group:
|
|
14
|
+
- `sce state plan`
|
|
15
|
+
- `sce state doctor`
|
|
16
|
+
- `sce state migrate` (dry-run default, `--apply` to write)
|
|
17
|
+
- `sce state export`
|
|
18
|
+
- New migration manager module: `lib/state/state-migration-manager.js`.
|
|
19
|
+
- SQLite index tables for staged migration of file-based registries:
|
|
20
|
+
- `agent_runtime_registry`
|
|
21
|
+
- `timeline_snapshot_registry`
|
|
22
|
+
- `scene_session_cycle_registry`
|
|
23
|
+
- `state_migration_registry`
|
|
24
|
+
- Unit tests for state migration planning/execution/export:
|
|
25
|
+
- `tests/unit/state/state-migration-manager.test.js`
|
|
26
|
+
- `tests/unit/commands/state.test.js`
|
|
27
|
+
- New reconciliation gate script for migration parity checks:
|
|
28
|
+
- `scripts/state-migration-reconciliation-gate.js`
|
|
29
|
+
- npm script: `gate:state-migration-reconciliation`
|
|
30
|
+
- Session runtime diagnostics expansion:
|
|
31
|
+
- new command: `sce session list`
|
|
32
|
+
- `sce session show/list` JSON includes `session_source` and `scene_index` consistency metadata
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Timeline runtime reads now prefer SQLite index entries (`timeline_snapshot_registry`) when available.
|
|
36
|
+
- Scene session listing views now prefer SQLite cycle index entries (`scene_session_cycle_registry`) when available.
|
|
37
|
+
- Agent registry writes now mirror to SQLite agent runtime index (`agent_runtime_registry`) as a best-effort sync.
|
|
38
|
+
- `sce state doctor` now emits aggregate `summary` metrics and runtime read diagnostics (`runtime.timeline`, `runtime.scene_session`) to expose sqlite/file drift status for IDE and CI gates.
|
|
39
|
+
- Startup compliance behavior hardened to avoid runtime side effects on read-only commands:
|
|
40
|
+
- `sce --version/-v`, `sce --help/-h`, `sce help`, and `sce version-info` now skip steering cleanup checks.
|
|
41
|
+
- steering compliance allowlist now includes `.sce/steering/manifest.yaml`, `compiled/`, and runtime lock/pending files.
|
|
42
|
+
|
|
43
|
+
## [3.6.3] - 2026-03-05
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- SQLite-backed write authorization lease flow:
|
|
47
|
+
- new policy baseline: `.sce/config/authorization-policy.json`
|
|
48
|
+
- new state tables: `auth_lease_registry`, `auth_event_stream`
|
|
49
|
+
- new commands:
|
|
50
|
+
- `sce auth grant`
|
|
51
|
+
- `sce auth status`
|
|
52
|
+
- `sce auth revoke`
|
|
53
|
+
- New write-authorization module: `lib/security/write-authorization.js`.
|
|
54
|
+
- New unit coverage:
|
|
55
|
+
- `tests/unit/security/write-authorization.test.js`
|
|
56
|
+
- `tests/unit/commands/auth.test.js`
|
|
57
|
+
- extended state-store auth lifecycle tests.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- `studio apply/release/rollback` now support `--auth-lease <lease-id>` and can enforce lease-based write authorization.
|
|
61
|
+
- `task rerun` now supports `--auth-lease <lease-id>` and can enforce lease-based write authorization on non-dry-run operations.
|
|
62
|
+
- Authorization checks and lease audit events are persisted to `.sce/state/sce-state.sqlite`.
|
|
63
|
+
|
|
10
64
|
## [3.6.2] - 2026-03-04
|
|
11
65
|
|
|
12
66
|
### Changed
|
package/README.md
CHANGED
|
@@ -145,6 +145,19 @@ Studio task-stream output contract (default):
|
|
|
145
145
|
- SQLite-only backend (`.sce/state/sce-state.sqlite`)
|
|
146
146
|
- In-memory fallback only in `NODE_ENV=test` or when `SCE_STATE_ALLOW_MEMORY_FALLBACK=1`
|
|
147
147
|
- Outside those conditions, unavailable SQLite support fails fast for task-ref/event persistence
|
|
148
|
+
- Gradual file-to-sqlite migration tooling:
|
|
149
|
+
- `sce state plan --json`
|
|
150
|
+
- `sce state doctor --json`
|
|
151
|
+
- `sce state migrate --all --apply --json`
|
|
152
|
+
- `sce state export --out .sce/reports/state-migration/state-export.latest.json --json`
|
|
153
|
+
- reconciliation gate: `npm run gate:state-migration-reconciliation`
|
|
154
|
+
- runtime reads now prefer sqlite indexes for timeline/scene-session views when indexed data exists
|
|
155
|
+
- `state doctor` now emits `summary` and runtime diagnostics (`runtime.timeline`, `runtime.scene_session`) with read-source and consistency status
|
|
156
|
+
- Write authorization lease model (SQLite-backed):
|
|
157
|
+
- policy file: `.sce/config/authorization-policy.json`
|
|
158
|
+
- grant lease: `sce auth grant --scope studio:* --reason "<reason>" --auth-password <password> --json`
|
|
159
|
+
- inspect/revoke: `sce auth status --json` / `sce auth revoke --lease <lease-id> --json`
|
|
160
|
+
- protected writes accept `--auth-lease <lease-id>` on `studio apply/release/rollback` and `task rerun`
|
|
148
161
|
|
|
149
162
|
---
|
|
150
163
|
|
|
@@ -201,5 +214,5 @@ MIT. See [LICENSE](LICENSE).
|
|
|
201
214
|
|
|
202
215
|
---
|
|
203
216
|
|
|
204
|
-
**Version**: 3.6.
|
|
205
|
-
**Last Updated**: 2026-03-
|
|
217
|
+
**Version**: 3.6.3
|
|
218
|
+
**Last Updated**: 2026-03-05
|
package/README.zh.md
CHANGED
|
@@ -145,6 +145,19 @@ Studio 任务流输出契约(默认):
|
|
|
145
145
|
- 仅支持 SQLite 后端(`.sce/state/sce-state.sqlite`)
|
|
146
146
|
- 仅在 `NODE_ENV=test` 或 `SCE_STATE_ALLOW_MEMORY_FALLBACK=1` 时允许内存回退
|
|
147
147
|
- 在上述条件之外若 SQLite 不可用,任务引用/事件持久化会快速失败
|
|
148
|
+
- 渐进式文件到 SQLite 迁移工具:
|
|
149
|
+
- `sce state plan --json`
|
|
150
|
+
- `sce state doctor --json`
|
|
151
|
+
- `sce state migrate --all --apply --json`
|
|
152
|
+
- `sce state export --out .sce/reports/state-migration/state-export.latest.json --json`
|
|
153
|
+
- 对账门禁:`npm run gate:state-migration-reconciliation`
|
|
154
|
+
- 运行时读取在存在索引数据时优先使用 SQLite(timeline/scene-session 视图)
|
|
155
|
+
- `state doctor` 新增 `summary` 与运行时诊断(`runtime.timeline`、`runtime.scene_session`),可直接读取读源与一致性状态
|
|
156
|
+
- 写入授权租约模型(SQLite 持久化):
|
|
157
|
+
- 策略文件:`.sce/config/authorization-policy.json`
|
|
158
|
+
- 申请租约:`sce auth grant --scope studio:* --reason "<原因>" --auth-password <密码> --json`
|
|
159
|
+
- 查看/撤销:`sce auth status --json` / `sce auth revoke --lease <lease-id> --json`
|
|
160
|
+
- 受保护写操作支持 `--auth-lease <lease-id>`:`studio apply/release/rollback`、`task rerun`
|
|
148
161
|
|
|
149
162
|
---
|
|
150
163
|
|
|
@@ -201,5 +214,5 @@ MIT,见 [LICENSE](LICENSE)。
|
|
|
201
214
|
|
|
202
215
|
---
|
|
203
216
|
|
|
204
|
-
**版本**:3.6.
|
|
205
|
-
**最后更新**:2026-03-
|
|
217
|
+
**版本**:3.6.3
|
|
218
|
+
**最后更新**:2026-03-05
|
|
@@ -24,6 +24,8 @@ const { registerSpecRelatedCommand } = require('../lib/commands/spec-related');
|
|
|
24
24
|
const { registerTimelineCommands } = require('../lib/commands/timeline');
|
|
25
25
|
const { registerValueCommands } = require('../lib/commands/value');
|
|
26
26
|
const { registerTaskCommands } = require('../lib/commands/task');
|
|
27
|
+
const { registerAuthCommands } = require('../lib/commands/auth');
|
|
28
|
+
const { registerStateCommands } = require('../lib/commands/state');
|
|
27
29
|
const VersionChecker = require('../lib/version/version-checker');
|
|
28
30
|
const {
|
|
29
31
|
findLegacyKiroDirectories,
|
|
@@ -160,6 +162,9 @@ function isLegacyMigrationAllowlistedCommand(args) {
|
|
|
160
162
|
if (command === 'help') {
|
|
161
163
|
return true;
|
|
162
164
|
}
|
|
165
|
+
if (command === 'version-info') {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
163
168
|
|
|
164
169
|
if (command === 'workspace') {
|
|
165
170
|
const subcommand = args[commandIndex + 1];
|
|
@@ -194,6 +199,30 @@ function isTakeoverAutoApplySkippedCommand(args) {
|
|
|
194
199
|
return subcommand === 'takeover-audit';
|
|
195
200
|
}
|
|
196
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Commands/options that must be read-only and have no runtime side effects.
|
|
204
|
+
*
|
|
205
|
+
* @param {string[]} args
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
function isNoMutationCommand(args) {
|
|
209
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (args.includes('-v') || args.includes('--version') || args.includes('-h') || args.includes('--help')) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const commandIndex = findCommandIndex(args);
|
|
218
|
+
if (commandIndex < 0) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const command = args[commandIndex];
|
|
223
|
+
return command === 'help' || command === 'version-info';
|
|
224
|
+
}
|
|
225
|
+
|
|
197
226
|
// 版本和基本信息
|
|
198
227
|
program
|
|
199
228
|
.name(t('cli.name'))
|
|
@@ -945,6 +974,8 @@ registerOrchestrateCommands(program);
|
|
|
945
974
|
// Value realization and observability commands
|
|
946
975
|
registerValueCommands(program);
|
|
947
976
|
registerTaskCommands(program);
|
|
977
|
+
registerAuthCommands(program);
|
|
978
|
+
registerStateCommands(program);
|
|
948
979
|
|
|
949
980
|
// Template management commands
|
|
950
981
|
const templatesCommand = require('../lib/commands/templates');
|
|
@@ -1072,7 +1103,8 @@ async function updateProjectConfig(projectName) {
|
|
|
1072
1103
|
const args = process.argv.slice(2);
|
|
1073
1104
|
const isLegacyAllowlistedCommand = isLegacyMigrationAllowlistedCommand(args);
|
|
1074
1105
|
const skipAutoTakeover = isTakeoverAutoApplySkippedCommand(args);
|
|
1075
|
-
const skipCheck = args
|
|
1106
|
+
const skipCheck = isNoMutationCommand(args) ||
|
|
1107
|
+
args.includes('--skip-steering-check') ||
|
|
1076
1108
|
process.env.KSE_SKIP_STEERING_CHECK === '1';
|
|
1077
1109
|
const forceCheck = args.includes('--force-steering-check');
|
|
1078
1110
|
|
|
@@ -232,12 +232,16 @@ sce session start "ship order workflow hardening" --tool codex --agent-version 1
|
|
|
232
232
|
sce session resume release-20260224 --status active
|
|
233
233
|
sce session snapshot release-20260224 --summary "post-gate checkpoint" --payload '{"tests_passed":42}' --json
|
|
234
234
|
sce session show release-20260224 --json
|
|
235
|
+
sce session list --limit 20 --json
|
|
235
236
|
```
|
|
236
237
|
|
|
237
238
|
Session governance defaults:
|
|
238
239
|
- `1 scene = 1 primary session` (managed by `studio plan --scene ...`)
|
|
239
240
|
- `spec` runs can bind as child sessions (`spec bootstrap|pipeline --scene <scene-id>`)
|
|
240
241
|
- successful `studio release` auto-archives current scene session and opens next cycle session
|
|
242
|
+
- `session show/list` JSON now includes runtime diagnostics:
|
|
243
|
+
- `session_source`
|
|
244
|
+
- `scene_index.status` (`aligned`, `pending-sync`, `sqlite-ahead`, etc.)
|
|
241
245
|
|
|
242
246
|
### Watch Mode
|
|
243
247
|
|
|
@@ -552,6 +556,8 @@ sce studio generate --scene scene.customer-order-inventory --target 331 --json
|
|
|
552
556
|
|
|
553
557
|
# Apply generated patch metadata
|
|
554
558
|
sce studio apply --patch-bundle patch-scene.customer-order-inventory-<timestamp> --json
|
|
559
|
+
# Apply with explicit write lease
|
|
560
|
+
sce studio apply --job <job-id> --auth-lease <lease-id> --json
|
|
555
561
|
|
|
556
562
|
# Record verification result
|
|
557
563
|
sce studio verify --profile standard --json
|
|
@@ -560,6 +566,8 @@ sce studio verify --profile strict --json
|
|
|
560
566
|
# Record release event
|
|
561
567
|
sce studio release --channel dev --profile standard --json
|
|
562
568
|
sce studio release --channel dev --profile strict --json
|
|
569
|
+
# Release with explicit write lease
|
|
570
|
+
sce studio release --job <job-id> --channel dev --auth-lease <lease-id> --json
|
|
563
571
|
|
|
564
572
|
# Resume from latest or explicit job
|
|
565
573
|
sce studio resume --job <job-id> --json
|
|
@@ -571,6 +579,8 @@ sce studio events --job <job-id> --openhands-events ./openhands-events.json --js
|
|
|
571
579
|
|
|
572
580
|
# Rollback a job after apply/release
|
|
573
581
|
sce studio rollback --job <job-id> --reason "manual-check-failed" --json
|
|
582
|
+
# Rollback with explicit write lease
|
|
583
|
+
sce studio rollback --job <job-id> --reason "manual-check-failed" --auth-lease <lease-id> --json
|
|
574
584
|
|
|
575
585
|
# Build scene-organized spec governance portfolio
|
|
576
586
|
sce studio portfolio --json
|
|
@@ -583,6 +593,11 @@ sce studio backfill-spec-scenes --scene scene.unassigned --limit 20 --apply --js
|
|
|
583
593
|
|
|
584
594
|
# Enforce authorization for a protected action
|
|
585
595
|
SCE_STUDIO_REQUIRE_AUTH=1 SCE_STUDIO_AUTH_PASSWORD=top-secret sce studio apply --job <job-id> --auth-password top-secret --json
|
|
596
|
+
|
|
597
|
+
# Grant/review/revoke write lease (stored in .sce/state/sce-state.sqlite)
|
|
598
|
+
SCE_AUTH_PASSWORD=top-secret sce auth grant --scope studio:* --reason "maintenance window" --auth-password top-secret --json
|
|
599
|
+
sce auth status --json
|
|
600
|
+
sce auth revoke --lease <lease-id> --reason "window closed" --json
|
|
586
601
|
```
|
|
587
602
|
|
|
588
603
|
Studio JSON output now includes a stable UI-oriented task stream contract (in addition to existing `job_*` fields):
|
|
@@ -689,6 +704,78 @@ Default policy file (recommended to commit): `.sce/config/studio-security.json`
|
|
|
689
704
|
}
|
|
690
705
|
```
|
|
691
706
|
|
|
707
|
+
State migration commands (gradual file -> sqlite indexing):
|
|
708
|
+
|
|
709
|
+
```bash
|
|
710
|
+
# inspect migratable file-based registries
|
|
711
|
+
sce state plan --json
|
|
712
|
+
|
|
713
|
+
# diagnose sqlite readiness and file/sqlite index sync
|
|
714
|
+
sce state doctor --json
|
|
715
|
+
|
|
716
|
+
# dry-run migration (default no write)
|
|
717
|
+
sce state migrate --all --json
|
|
718
|
+
|
|
719
|
+
# apply migration writes into sqlite index tables
|
|
720
|
+
sce state migrate --all --apply --json
|
|
721
|
+
|
|
722
|
+
# migrate specific components
|
|
723
|
+
sce state migrate --component collab.agent-registry --component runtime.timeline-index --apply --json
|
|
724
|
+
|
|
725
|
+
# export sqlite migration tables for audit/debug
|
|
726
|
+
sce state export --out .sce/reports/state-migration/state-export.latest.json --json
|
|
727
|
+
|
|
728
|
+
# reconciliation gate (non-blocking by default; choose strict flags as needed)
|
|
729
|
+
npm run gate:state-migration-reconciliation
|
|
730
|
+
node scripts/state-migration-reconciliation-gate.js --fail-on-alert --fail-on-pending --json
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Current migratable components:
|
|
734
|
+
- `collab.agent-registry` (`.sce/config/agent-registry.json`)
|
|
735
|
+
- `runtime.timeline-index` (`.sce/timeline/index.json`)
|
|
736
|
+
- `runtime.scene-session-index` (`.sce/session-governance/scene-index.json`)
|
|
737
|
+
|
|
738
|
+
SQLite index tables introduced for gradual migration:
|
|
739
|
+
- `agent_runtime_registry`
|
|
740
|
+
- `timeline_snapshot_registry`
|
|
741
|
+
- `scene_session_cycle_registry`
|
|
742
|
+
- `state_migration_registry`
|
|
743
|
+
|
|
744
|
+
Runtime read preference:
|
|
745
|
+
- timeline/session runtime views now prefer SQLite indexes when indexed rows exist.
|
|
746
|
+
- file artifacts remain source-of-truth for content payload and recovery operations.
|
|
747
|
+
- `sce state doctor --json` now includes:
|
|
748
|
+
- `summary` aggregate (`pending_components`, `total_record_drift`, `blocking_count`, `alert_count`)
|
|
749
|
+
- runtime read diagnostics (`runtime.timeline`, `runtime.scene_session`) with read-source/read-preference and consistency status
|
|
750
|
+
|
|
751
|
+
Write lease model (optional, policy-driven, SQLite-backed):
|
|
752
|
+
- Policy file: `.sce/config/authorization-policy.json`
|
|
753
|
+
- Lease/event persistence: `.sce/state/sce-state.sqlite` (`auth_lease_registry`, `auth_event_stream`)
|
|
754
|
+
- Default protected actions: `studio:apply`, `studio:release`, `studio:rollback`, `task:rerun`
|
|
755
|
+
- Default lease TTL: 15 minutes (`default_ttl_minutes`)
|
|
756
|
+
- Write commands accept `--auth-lease <lease-id>`
|
|
757
|
+
- Policy env overrides:
|
|
758
|
+
- `SCE_AUTH_REQUIRE_LEASE=1`
|
|
759
|
+
- `SCE_AUTH_PASSWORD_ENV=<ENV_NAME>`
|
|
760
|
+
- `SCE_AUTH_ENFORCE_ACTIONS=studio:apply,task:rerun`
|
|
761
|
+
|
|
762
|
+
Default write authorization policy (recommended to commit): `.sce/config/authorization-policy.json`
|
|
763
|
+
|
|
764
|
+
```json
|
|
765
|
+
{
|
|
766
|
+
"enabled": false,
|
|
767
|
+
"enforce_actions": ["studio:apply", "studio:release", "studio:rollback", "task:rerun"],
|
|
768
|
+
"default_ttl_minutes": 15,
|
|
769
|
+
"max_ttl_minutes": 120,
|
|
770
|
+
"require_password_for_grant": true,
|
|
771
|
+
"require_password_for_revoke": false,
|
|
772
|
+
"password_env": "SCE_AUTH_PASSWORD",
|
|
773
|
+
"default_scope": ["project:*"],
|
|
774
|
+
"allow_test_bypass": true,
|
|
775
|
+
"allow_password_as_inline_lease": false
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
692
779
|
Studio intake policy file (default, recommended to commit): `.sce/config/studio-intake-policy.json`
|
|
693
780
|
|
|
694
781
|
```json
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fsUtils = require('../utils/fs-utils');
|
|
13
13
|
const { MultiAgentConfig } = require('./multi-agent-config');
|
|
14
|
+
const { getSceStateStore } = require('../state/sce-state-store');
|
|
14
15
|
|
|
15
16
|
const REGISTRY_FILENAME = 'agent-registry.json';
|
|
16
17
|
const CONFIG_DIR = '.sce/config';
|
|
@@ -26,13 +27,16 @@ class AgentRegistry {
|
|
|
26
27
|
* @param {import('../lock/machine-identifier').MachineIdentifier} machineIdentifier
|
|
27
28
|
* @param {import('../lock/task-lock-manager').TaskLockManager|null} [taskLockManager=null] - Optional, injected to avoid circular deps
|
|
28
29
|
*/
|
|
29
|
-
constructor(workspaceRoot, machineIdentifier, taskLockManager = null) {
|
|
30
|
+
constructor(workspaceRoot, machineIdentifier, taskLockManager = null, options = {}) {
|
|
30
31
|
this._workspaceRoot = workspaceRoot;
|
|
31
32
|
this._machineIdentifier = machineIdentifier;
|
|
32
33
|
this._taskLockManager = taskLockManager;
|
|
33
34
|
this._registryPath = path.join(workspaceRoot, CONFIG_DIR, REGISTRY_FILENAME);
|
|
34
35
|
this._configDir = path.join(workspaceRoot, CONFIG_DIR);
|
|
35
36
|
this._multiAgentConfig = new MultiAgentConfig(workspaceRoot);
|
|
37
|
+
this._stateStore = options.stateStore || getSceStateStore(workspaceRoot, {
|
|
38
|
+
env: options.env || process.env
|
|
39
|
+
});
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
@@ -67,6 +71,39 @@ class AgentRegistry {
|
|
|
67
71
|
async _writeRegistry(registry) {
|
|
68
72
|
await fsUtils.ensureDirectory(this._configDir);
|
|
69
73
|
await fsUtils.writeJSON(this._registryPath, registry);
|
|
74
|
+
await this._syncRegistryToStateStore(registry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_mapRegistryRows(registry = {}) {
|
|
78
|
+
if (!registry || typeof registry !== 'object' || !registry.agents || typeof registry.agents !== 'object') {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return Object.values(registry.agents)
|
|
82
|
+
.map((agent) => ({
|
|
83
|
+
agent_id: `${agent && agent.agentId ? agent.agentId : ''}`.trim(),
|
|
84
|
+
machine_id: `${agent && agent.machineId ? agent.machineId : ''}`.trim(),
|
|
85
|
+
instance_index: Number.parseInt(`${agent && agent.instanceIndex}`, 10) || 0,
|
|
86
|
+
hostname: `${agent && agent.hostname ? agent.hostname : ''}`.trim() || null,
|
|
87
|
+
registered_at: `${agent && agent.registeredAt ? agent.registeredAt : ''}`.trim() || null,
|
|
88
|
+
last_heartbeat: `${agent && agent.lastHeartbeat ? agent.lastHeartbeat : ''}`.trim() || null,
|
|
89
|
+
status: `${agent && agent.status ? agent.status : ''}`.trim() || null,
|
|
90
|
+
current_task: agent && typeof agent.currentTask === 'object' ? agent.currentTask : null
|
|
91
|
+
}))
|
|
92
|
+
.filter((row) => row.agent_id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _syncRegistryToStateStore(registry = {}) {
|
|
96
|
+
if (!this._stateStore) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const rows = this._mapRegistryRows(registry);
|
|
101
|
+
await this._stateStore.upsertAgentRuntimeRecords(rows, {
|
|
102
|
+
source: 'collab.agent-registry'
|
|
103
|
+
});
|
|
104
|
+
} catch (_error) {
|
|
105
|
+
// best effort only, file registry remains source
|
|
106
|
+
}
|
|
70
107
|
}
|
|
71
108
|
|
|
72
109
|
/**
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const {
|
|
4
|
+
grantWriteAuthorizationLease,
|
|
5
|
+
revokeWriteAuthorizationLease,
|
|
6
|
+
collectWriteAuthorizationStatus,
|
|
7
|
+
getWriteAuthorizationLease
|
|
8
|
+
} = require('../security/write-authorization');
|
|
9
|
+
|
|
10
|
+
function normalizeString(value) {
|
|
11
|
+
if (typeof value !== 'string') {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
return value.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeInteger(value, fallback = 0) {
|
|
18
|
+
const parsed = Number.parseInt(`${value}`, 10);
|
|
19
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeScopeInput(value) {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
const text = normalizeString(value);
|
|
30
|
+
if (!text) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return text.split(/[,\s]+/g).map((item) => normalizeString(item)).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printAuthPayload(payload, options = {}) {
|
|
37
|
+
if (options.json) {
|
|
38
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mode = normalizeString(payload.mode);
|
|
43
|
+
if (mode === 'auth-grant') {
|
|
44
|
+
console.log(chalk.blue(`Auth lease granted: ${payload.lease.lease_id}`));
|
|
45
|
+
console.log(` Subject: ${payload.lease.subject}`);
|
|
46
|
+
console.log(` Role: ${payload.lease.role}`);
|
|
47
|
+
console.log(` Scope: ${(payload.lease.scope || []).join(', ') || 'n/a'}`);
|
|
48
|
+
console.log(` Expires: ${payload.lease.expires_at || 'n/a'}`);
|
|
49
|
+
console.log(` Store: ${payload.store_path || 'n/a'}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (mode === 'auth-revoke') {
|
|
54
|
+
console.log(chalk.blue(`Auth lease revoked: ${payload.lease.lease_id}`));
|
|
55
|
+
console.log(` Subject: ${payload.lease.subject}`);
|
|
56
|
+
console.log(` Revoked at: ${payload.lease.revoked_at || 'n/a'}`);
|
|
57
|
+
console.log(` Store: ${payload.store_path || 'n/a'}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (mode === 'auth-status') {
|
|
62
|
+
if (payload.lease) {
|
|
63
|
+
console.log(chalk.blue(`Auth lease: ${payload.lease.lease_id}`));
|
|
64
|
+
console.log(` Subject: ${payload.lease.subject}`);
|
|
65
|
+
console.log(` Role: ${payload.lease.role}`);
|
|
66
|
+
console.log(` Scope: ${(payload.lease.scope || []).join(', ') || 'n/a'}`);
|
|
67
|
+
console.log(` Expires: ${payload.lease.expires_at || 'n/a'}`);
|
|
68
|
+
console.log(` Revoked: ${payload.lease.revoked_at || 'no'}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.log(chalk.blue('Auth lease status'));
|
|
71
|
+
console.log(` Active leases: ${payload.summary.active_lease_count}`);
|
|
72
|
+
console.log(` Events: ${payload.summary.event_count}`);
|
|
73
|
+
}
|
|
74
|
+
console.log(` Store: ${payload.store_path || 'n/a'}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runAuthGrantCommand(options = {}, dependencies = {}) {
|
|
79
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
80
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
81
|
+
const env = dependencies.env || process.env;
|
|
82
|
+
|
|
83
|
+
const granted = await grantWriteAuthorizationLease({
|
|
84
|
+
subject: options.subject,
|
|
85
|
+
role: options.role,
|
|
86
|
+
scope: normalizeScopeInput(options.scope),
|
|
87
|
+
ttlMinutes: normalizeInteger(options.ttlMinutes, 15),
|
|
88
|
+
reason: options.reason,
|
|
89
|
+
authPassword: options.authPassword,
|
|
90
|
+
actor: options.actor,
|
|
91
|
+
metadata: {
|
|
92
|
+
source: 'sce auth grant'
|
|
93
|
+
}
|
|
94
|
+
}, {
|
|
95
|
+
projectPath,
|
|
96
|
+
fileSystem,
|
|
97
|
+
env,
|
|
98
|
+
authSecret: dependencies.authSecret
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const payload = {
|
|
102
|
+
mode: 'auth-grant',
|
|
103
|
+
success: true,
|
|
104
|
+
policy: granted.policy,
|
|
105
|
+
policy_path: granted.policy_path,
|
|
106
|
+
lease: granted.lease,
|
|
107
|
+
store_path: granted.store_path
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
printAuthPayload(payload, options);
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runAuthStatusCommand(options = {}, dependencies = {}) {
|
|
115
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
116
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
117
|
+
const env = dependencies.env || process.env;
|
|
118
|
+
const leaseId = normalizeString(options.lease);
|
|
119
|
+
|
|
120
|
+
if (leaseId) {
|
|
121
|
+
const [status, lease] = await Promise.all([
|
|
122
|
+
collectWriteAuthorizationStatus({
|
|
123
|
+
activeOnly: options.all !== true,
|
|
124
|
+
limit: normalizeInteger(options.limit, 20),
|
|
125
|
+
eventsLimit: normalizeInteger(options.eventsLimit, 20)
|
|
126
|
+
}, {
|
|
127
|
+
projectPath,
|
|
128
|
+
fileSystem,
|
|
129
|
+
env
|
|
130
|
+
}),
|
|
131
|
+
getWriteAuthorizationLease(leaseId, {
|
|
132
|
+
projectPath,
|
|
133
|
+
fileSystem,
|
|
134
|
+
env
|
|
135
|
+
})
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const payload = {
|
|
139
|
+
mode: 'auth-status',
|
|
140
|
+
success: true,
|
|
141
|
+
policy: status.policy,
|
|
142
|
+
policy_path: status.policy_path,
|
|
143
|
+
lease: lease || null,
|
|
144
|
+
summary: {
|
|
145
|
+
active_lease_count: Array.isArray(status.leases) ? status.leases.length : 0,
|
|
146
|
+
event_count: Array.isArray(status.events) ? status.events.length : 0
|
|
147
|
+
},
|
|
148
|
+
events: Array.isArray(status.events) ? status.events : [],
|
|
149
|
+
store_path: status.store_path
|
|
150
|
+
};
|
|
151
|
+
printAuthPayload(payload, options);
|
|
152
|
+
return payload;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const status = await collectWriteAuthorizationStatus({
|
|
156
|
+
activeOnly: options.all !== true,
|
|
157
|
+
limit: normalizeInteger(options.limit, 20),
|
|
158
|
+
eventsLimit: normalizeInteger(options.eventsLimit, 20)
|
|
159
|
+
}, {
|
|
160
|
+
projectPath,
|
|
161
|
+
fileSystem,
|
|
162
|
+
env
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const payload = {
|
|
166
|
+
mode: 'auth-status',
|
|
167
|
+
success: true,
|
|
168
|
+
policy: status.policy,
|
|
169
|
+
policy_path: status.policy_path,
|
|
170
|
+
leases: Array.isArray(status.leases) ? status.leases : [],
|
|
171
|
+
events: Array.isArray(status.events) ? status.events : [],
|
|
172
|
+
summary: {
|
|
173
|
+
active_lease_count: Array.isArray(status.leases) ? status.leases.length : 0,
|
|
174
|
+
event_count: Array.isArray(status.events) ? status.events.length : 0
|
|
175
|
+
},
|
|
176
|
+
store_path: status.store_path
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
printAuthPayload(payload, options);
|
|
180
|
+
return payload;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function runAuthRevokeCommand(options = {}, dependencies = {}) {
|
|
184
|
+
const leaseId = normalizeString(options.lease);
|
|
185
|
+
if (!leaseId) {
|
|
186
|
+
throw new Error('--lease is required');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
190
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
191
|
+
const env = dependencies.env || process.env;
|
|
192
|
+
|
|
193
|
+
const revoked = await revokeWriteAuthorizationLease(leaseId, {
|
|
194
|
+
authPassword: options.authPassword,
|
|
195
|
+
reason: options.reason,
|
|
196
|
+
actor: options.actor
|
|
197
|
+
}, {
|
|
198
|
+
projectPath,
|
|
199
|
+
fileSystem,
|
|
200
|
+
env,
|
|
201
|
+
authSecret: dependencies.authSecret
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const payload = {
|
|
205
|
+
mode: 'auth-revoke',
|
|
206
|
+
success: true,
|
|
207
|
+
policy: revoked.policy,
|
|
208
|
+
policy_path: revoked.policy_path,
|
|
209
|
+
lease: revoked.lease,
|
|
210
|
+
store_path: revoked.store_path
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
printAuthPayload(payload, options);
|
|
214
|
+
return payload;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function runAuthCommand(handler, options = {}, context = 'auth') {
|
|
218
|
+
Promise.resolve(handler(options))
|
|
219
|
+
.catch((error) => {
|
|
220
|
+
console.error(chalk.red(`${context} failed: ${error.message}`));
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function registerAuthCommands(program) {
|
|
226
|
+
const auth = program
|
|
227
|
+
.command('auth')
|
|
228
|
+
.description('Manage temporary write authorization leases');
|
|
229
|
+
|
|
230
|
+
auth
|
|
231
|
+
.command('grant')
|
|
232
|
+
.description('Grant a write authorization lease (persisted in sqlite)')
|
|
233
|
+
.option('--subject <subject>', 'Lease subject (default: current user)')
|
|
234
|
+
.option('--role <role>', 'Subject role', 'maintainer')
|
|
235
|
+
.option('--scope <scope>', 'Scope list, comma-separated (example: studio:*,task:rerun)')
|
|
236
|
+
.option('--ttl-minutes <minutes>', 'Lease TTL in minutes', '15')
|
|
237
|
+
.option('--reason <reason>', 'Grant reason')
|
|
238
|
+
.option('--actor <actor>', 'Audit actor override')
|
|
239
|
+
.option('--auth-password <password>', 'Authorization password for grant policy')
|
|
240
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
241
|
+
.action((options) => runAuthCommand(runAuthGrantCommand, options, 'auth grant'));
|
|
242
|
+
|
|
243
|
+
auth
|
|
244
|
+
.command('status')
|
|
245
|
+
.description('Show current authorization lease and event status from sqlite')
|
|
246
|
+
.option('--lease <lease-id>', 'Inspect one lease id')
|
|
247
|
+
.option('--all', 'Include inactive/revoked leases')
|
|
248
|
+
.option('--limit <n>', 'Lease result limit', '20')
|
|
249
|
+
.option('--events-limit <n>', 'Auth event result limit', '20')
|
|
250
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
251
|
+
.action((options) => runAuthCommand(runAuthStatusCommand, options, 'auth status'));
|
|
252
|
+
|
|
253
|
+
auth
|
|
254
|
+
.command('revoke')
|
|
255
|
+
.description('Revoke a write authorization lease')
|
|
256
|
+
.requiredOption('--lease <lease-id>', 'Lease id')
|
|
257
|
+
.option('--reason <reason>', 'Revoke reason')
|
|
258
|
+
.option('--actor <actor>', 'Audit actor override')
|
|
259
|
+
.option('--auth-password <password>', 'Authorization password for revoke policy')
|
|
260
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
261
|
+
.action((options) => runAuthCommand(runAuthRevokeCommand, options, 'auth revoke'));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
runAuthGrantCommand,
|
|
266
|
+
runAuthStatusCommand,
|
|
267
|
+
runAuthRevokeCommand,
|
|
268
|
+
registerAuthCommands
|
|
269
|
+
};
|