@haaaiawd/second-nature 0.2.1 → 0.2.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/index.js +5 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +23 -0
- package/runtime/cli/ops/heartbeat-surface.js +73 -1
- package/runtime/cli/ops/manual-run-dispatcher.d.ts +2 -0
- package/runtime/cli/ops/manual-run-dispatcher.js +10 -0
- package/runtime/cli/ops/ops-router.js +117 -31
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +3 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -0
- package/runtime/connectors/base/contract.d.ts +10 -0
- package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
- package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +336 -25
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +33 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +41 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +6 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +11 -0
- package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
- package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
- package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
- package/runtime/core/second-nature/perception/perception-builder.js +18 -7
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +3 -0
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +42 -1
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +157 -0
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +17 -16
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
- package/runtime/observability/living-loop-health-gate.d.ts +45 -0
- package/runtime/observability/living-loop-health-gate.js +94 -0
- package/runtime/observability/loop-status.d.ts +11 -0
- package/runtime/observability/loop-status.js +49 -3
- package/runtime/observability/services/audit-closure-recorders.d.ts +31 -0
- package/runtime/observability/services/audit-closure-recorders.js +87 -0
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +12 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +22 -3
- package/runtime/shared/types/v8-contracts.d.ts +2 -2
- package/runtime/storage/db/index.js +34 -0
- package/runtime/storage/db/migrations/index.js +4 -0
- package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
- package/runtime/storage/db/schema/v8-entities.d.ts +586 -0
- package/runtime/storage/db/schema/v8-entities.js +39 -0
- package/runtime/storage/v8-state-stores.d.ts +32 -2
- package/runtime/storage/v8-state-stores.js +121 -2
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
package/runtime/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createActionBridge } from "./action-bridge.js";
|
|
|
5
5
|
import { createCliCommands, } from "./commands/index.js";
|
|
6
6
|
import { createOpsRouter } from "./ops/ops-router.js";
|
|
7
7
|
import { createCliReadModels, } from "./read-models/index.js";
|
|
8
|
+
import { AppendOnlyAuditStore } from "../observability/audit/append-only-audit-store.js";
|
|
8
9
|
import { resolvePackagedRuntime } from "./runtime/runtime-artifact-boundary.js";
|
|
9
10
|
import { createRestoreSnapshotStore, } from "../storage/services/restore-snapshot-store.js";
|
|
10
11
|
import { createRuntimeDecisionRecorder, } from "../observability/services/runtime-decision-recorder.js";
|
|
@@ -221,12 +222,13 @@ export function createCliRuntimeDeps(overrides = {}) {
|
|
|
221
222
|
const stateDb = overrides.stateDb ?? createStateDatabase();
|
|
222
223
|
const observabilityDb = overrides.observabilityDb ?? createObservabilityDatabase();
|
|
223
224
|
const stateApi = overrides.stateApi ?? createStateAPI(stateDb);
|
|
225
|
+
const auditStore = overrides.auditStore ?? overrides.livedExperienceAuditStore ?? new AppendOnlyAuditStore();
|
|
224
226
|
const readModels = overrides.readModels ??
|
|
225
227
|
createCliReadModels({
|
|
226
228
|
stateDb,
|
|
227
229
|
observabilityDb,
|
|
228
230
|
workspaceRoot,
|
|
229
|
-
livedExperienceAuditStore:
|
|
231
|
+
livedExperienceAuditStore: auditStore,
|
|
230
232
|
});
|
|
231
233
|
const actionBridge = overrides.actionBridge ?? createActionBridge(stateApi);
|
|
232
234
|
const runtimeRecorder = overrides.runtimeRecorder ?? createRuntimeDecisionRecorder(observabilityDb);
|
|
@@ -261,6 +263,7 @@ export function createCliRuntimeDeps(overrides = {}) {
|
|
|
261
263
|
narrativeTimelineDeps,
|
|
262
264
|
secretAnchorDeps,
|
|
263
265
|
restoreSnapshotStore,
|
|
266
|
+
auditStore,
|
|
264
267
|
};
|
|
265
268
|
}
|
|
266
269
|
export function createCommandRouter(options = {}) {
|
|
@@ -281,6 +284,7 @@ export function createCommandRouter(options = {}) {
|
|
|
281
284
|
narrativeTimelineDeps: runtime.narrativeTimelineDeps,
|
|
282
285
|
secretAnchorDeps: runtime.secretAnchorDeps,
|
|
283
286
|
restoreSnapshotStore: runtime.restoreSnapshotStore,
|
|
287
|
+
auditStore: runtime.auditStore,
|
|
284
288
|
});
|
|
285
289
|
const commands = createCliCommands({
|
|
286
290
|
readModels: runtime.readModels,
|
|
@@ -18,6 +18,8 @@ import type { HeartbeatDigestAssemblerDeps } from "../../observability/services/
|
|
|
18
18
|
import type { GoalLifecyclePolicy } from "../../core/second-nature/heartbeat/goal-lifecycle-policy.js";
|
|
19
19
|
import type { IdleCuriosityPolicy } from "../../core/second-nature/heartbeat/idle-curiosity-policy.js";
|
|
20
20
|
import type { CircuitBreakerManager } from "../../core/second-nature/body/circuit-breaker/circuit-breaker-manager.js";
|
|
21
|
+
import type { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
|
|
22
|
+
import { type RealRuntimeSpineResult } from "../../core/second-nature/control-plane/real-runtime-spine.js";
|
|
21
23
|
export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
|
|
22
24
|
export interface HeartbeatSurfaceResult {
|
|
23
25
|
ok: boolean;
|
|
@@ -32,6 +34,20 @@ export interface HeartbeatSurfaceResult {
|
|
|
32
34
|
livedExperienceLoopClaimed: boolean;
|
|
33
35
|
/** True when structured fields mirror a fake adapter for schema parity only */
|
|
34
36
|
schemaParityOnly?: boolean;
|
|
37
|
+
/** T-CP.R.2: v8 real runtime spine result when state-backed action-closure spine ran */
|
|
38
|
+
v8Spine?: RealRuntimeSpineResult & {
|
|
39
|
+
degradedReason?: string;
|
|
40
|
+
};
|
|
41
|
+
/** T-GVS.R.1: agent-facing impulse context artifact read pointer */
|
|
42
|
+
impulseContext?: {
|
|
43
|
+
available: boolean;
|
|
44
|
+
sceneType?: string;
|
|
45
|
+
capabilityClass?: string | null;
|
|
46
|
+
impulseText?: string | null;
|
|
47
|
+
atmosphereText?: string | null;
|
|
48
|
+
freshnessMs?: number;
|
|
49
|
+
missingReason?: string;
|
|
50
|
+
};
|
|
35
51
|
}
|
|
36
52
|
export interface HeartbeatCheckInput {
|
|
37
53
|
probeOnly?: boolean;
|
|
@@ -80,5 +96,12 @@ export interface HeartbeatCheckInput {
|
|
|
80
96
|
idleCuriosityPolicy?: IdleCuriosityPolicy;
|
|
81
97
|
/** v7 T-BTS.C.5: circuit breaker manager for connector execution health. */
|
|
82
98
|
circuitBreakerManager?: CircuitBreakerManager;
|
|
99
|
+
/** T-OBS.R.1: shared audit sink for connector/Quiet events consumed by heartbeat_digest. */
|
|
100
|
+
auditStore?: AppendOnlyAuditStore;
|
|
101
|
+
/**
|
|
102
|
+
* T-CP.R.2: when true and state DB is wired, runs the v8 real runtime action-closure spine
|
|
103
|
+
* in addition to the v7 heartbeat loop. Produces state-backed closure/no-action records.
|
|
104
|
+
*/
|
|
105
|
+
v8SpineEnabled?: boolean;
|
|
83
106
|
}
|
|
84
107
|
export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createWorkspaceHeartbeatRunner } from "./workspace-heartbeat-runner.js";
|
|
2
|
+
// T-CP.R.2: v8 real runtime spine bridge
|
|
3
|
+
import { runRealRuntimeHeartbeatCycle, } from "../../core/second-nature/control-plane/real-runtime-spine.js";
|
|
2
4
|
function mapCycleToSurface(cycle, surfaceMode) {
|
|
3
5
|
const status = cycle.status === "runtime_carrier_only"
|
|
4
6
|
? "runtime_carrier_only"
|
|
@@ -82,10 +84,80 @@ export async function heartbeatCheck(input) {
|
|
|
82
84
|
goalLifecyclePolicy: input.goalLifecyclePolicy,
|
|
83
85
|
idleCuriosityPolicy: input.idleCuriosityPolicy,
|
|
84
86
|
circuitBreakerManager: input.circuitBreakerManager,
|
|
87
|
+
auditStore: input.auditStore,
|
|
85
88
|
});
|
|
86
89
|
try {
|
|
87
90
|
const cycle = await run(signal);
|
|
88
|
-
|
|
91
|
+
const surfaceResult = mapCycleToSurface(cycle, "workspace_full_runtime");
|
|
92
|
+
// T-CP.R.2: run v8 real runtime spine when enabled and state is available
|
|
93
|
+
if (input.v8SpineEnabled && input.state && input.workspaceRoot) {
|
|
94
|
+
try {
|
|
95
|
+
const v8Result = await runRealRuntimeHeartbeatCycle({
|
|
96
|
+
workspaceRoot: input.workspaceRoot,
|
|
97
|
+
state: input.state,
|
|
98
|
+
requestedAt: timestamp,
|
|
99
|
+
trigger: "host",
|
|
100
|
+
});
|
|
101
|
+
if ("status" in v8Result && v8Result.status === "degraded") {
|
|
102
|
+
surfaceResult.v8Spine = {
|
|
103
|
+
cycleId: "",
|
|
104
|
+
cycleSequence: 0,
|
|
105
|
+
degradedReason: v8Result.reason,
|
|
106
|
+
};
|
|
107
|
+
surfaceResult.reasons = [
|
|
108
|
+
...surfaceResult.reasons,
|
|
109
|
+
`v8_spine_degraded:${v8Result.reason}`,
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const spine = v8Result;
|
|
114
|
+
surfaceResult.v8Spine = spine;
|
|
115
|
+
surfaceResult.reasons = [
|
|
116
|
+
...surfaceResult.reasons,
|
|
117
|
+
`v8_spine_cycle:${spine.cycleId}`,
|
|
118
|
+
spine.closureRef
|
|
119
|
+
? "v8_closure_recorded"
|
|
120
|
+
: `v8_no_action:${spine.noActionReason ?? "unknown"}`,
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (v8Err) {
|
|
125
|
+
const v8Msg = v8Err instanceof Error ? v8Err.message : String(v8Err);
|
|
126
|
+
surfaceResult.reasons = [
|
|
127
|
+
...surfaceResult.reasons,
|
|
128
|
+
`v8_spine_exception:${v8Msg.slice(0, 120)}`,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// T-GVS.R.1: expose impulse context artifact when state is available
|
|
133
|
+
if (input.state) {
|
|
134
|
+
try {
|
|
135
|
+
const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
|
|
136
|
+
const ctx = await readImpulseContext(input.state, "social");
|
|
137
|
+
if (ctx.available) {
|
|
138
|
+
surfaceResult.impulseContext = {
|
|
139
|
+
available: true,
|
|
140
|
+
sceneType: ctx.artifact.sceneType,
|
|
141
|
+
capabilityClass: ctx.artifact.capabilityClass,
|
|
142
|
+
impulseText: ctx.artifact.impulseText,
|
|
143
|
+
atmosphereText: ctx.artifact.atmosphereText,
|
|
144
|
+
freshnessMs: ctx.freshnessMs,
|
|
145
|
+
};
|
|
146
|
+
surfaceResult.reasons.push(`impulse_context:${ctx.artifact.id}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
surfaceResult.impulseContext = {
|
|
150
|
+
available: false,
|
|
151
|
+
missingReason: ctx.reason,
|
|
152
|
+
};
|
|
153
|
+
surfaceResult.reasons.push(`impulse_context_missing:${ctx.reason}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Non-fatal: impulse context is advisory
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return surfaceResult;
|
|
89
161
|
}
|
|
90
162
|
catch (err) {
|
|
91
163
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -26,6 +26,7 @@ import type { ConnectorExecutor, ConnectorResult } from "../../connectors/base/c
|
|
|
26
26
|
import type { ExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
|
|
27
27
|
import type { WetProbeRunner } from "../../connectors/base/wet-probe-runner.js";
|
|
28
28
|
import type { CapabilityContractRegistryV7 } from "../../connectors/base/manifest-v7.js";
|
|
29
|
+
import type { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
|
|
29
30
|
import { type HeartbeatCheckInput, type HeartbeatSurfaceResult } from "./heartbeat-surface.js";
|
|
30
31
|
import type { RuntimeOpsEnvelope } from "./ops-router.js";
|
|
31
32
|
export interface ManualTriggerContext {
|
|
@@ -75,5 +76,6 @@ export interface ManualRunDispatcherDeps {
|
|
|
75
76
|
experienceWriter: ExperienceWriter;
|
|
76
77
|
wetProbeRunner: WetProbeRunner;
|
|
77
78
|
registryV7: CapabilityContractRegistryV7;
|
|
79
|
+
auditStore?: AppendOnlyAuditStore;
|
|
78
80
|
}
|
|
79
81
|
export declare function createManualRunDispatcher(deps: ManualRunDispatcherDeps): ManualRunDispatcher;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* Test coverage: tests/unit/ops/manual-run-dispatcher.test.ts
|
|
24
24
|
*/
|
|
25
25
|
import * as crypto from "node:crypto";
|
|
26
|
+
import { recordConnectorAttemptAudit } from "../../observability/services/audit-closure-recorders.js";
|
|
26
27
|
import { heartbeatCheck, } from "./heartbeat-surface.js";
|
|
27
28
|
function buildManualContext(input) {
|
|
28
29
|
return {
|
|
@@ -54,6 +55,15 @@ export function createManualRunDispatcher(deps) {
|
|
|
54
55
|
result: connectorResult,
|
|
55
56
|
triggerSource: ctx.triggerSource,
|
|
56
57
|
});
|
|
58
|
+
recordConnectorAttemptAudit({
|
|
59
|
+
auditStore: deps.auditStore,
|
|
60
|
+
platformId: input.platformId,
|
|
61
|
+
capability: input.capabilityId,
|
|
62
|
+
result: connectorResult,
|
|
63
|
+
triggerSource: ctx.triggerSource,
|
|
64
|
+
decisionId,
|
|
65
|
+
intentId,
|
|
66
|
+
});
|
|
57
67
|
const runResult = {
|
|
58
68
|
connectorResult,
|
|
59
69
|
experienceId,
|
|
@@ -28,6 +28,7 @@ import { writeRestoreAudit, } from "../../observability/services/restore-audit-s
|
|
|
28
28
|
import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
|
|
29
29
|
// v8 T-ROS.C.1: loop_status read model
|
|
30
30
|
import { readLoopStatus } from "../../observability/loop-status.js";
|
|
31
|
+
import { checkRealRunHealth } from "../../observability/living-loop-health-gate.js";
|
|
31
32
|
// T-ROS.C.3: ManualRunDispatcher and its deps
|
|
32
33
|
import { createManualRunDispatcher, } from "./manual-run-dispatcher.js";
|
|
33
34
|
import { createExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
|
|
@@ -441,9 +442,12 @@ export function createOpsRouter(deps) {
|
|
|
441
442
|
experienceWriter,
|
|
442
443
|
digestOpts,
|
|
443
444
|
dreamSchedulePort,
|
|
445
|
+
auditStore: deps.auditStore,
|
|
444
446
|
goalLifecyclePolicy,
|
|
445
447
|
idleCuriosityPolicy,
|
|
446
448
|
circuitBreakerManager,
|
|
449
|
+
v8SpineEnabled: input
|
|
450
|
+
?.v8SpineEnabled ?? (deps.state !== undefined),
|
|
447
451
|
});
|
|
448
452
|
if (result.ok &&
|
|
449
453
|
result.surfaceMode === "workspace_full_runtime" &&
|
|
@@ -780,6 +784,7 @@ export function createOpsRouter(deps) {
|
|
|
780
784
|
experienceWriter,
|
|
781
785
|
wetProbeRunner,
|
|
782
786
|
registryV7,
|
|
787
|
+
auditStore: deps.auditStore,
|
|
783
788
|
});
|
|
784
789
|
return dispatcher.runConnector({
|
|
785
790
|
platformId,
|
|
@@ -1051,6 +1056,33 @@ export function createOpsRouter(deps) {
|
|
|
1051
1056
|
...deps.heartbeatDigestDeps,
|
|
1052
1057
|
};
|
|
1053
1058
|
const digest = await generateHeartbeatDigest(date, digestDeps);
|
|
1059
|
+
// T-OBS.R.3: Embed real-run health into digest when state DB is available
|
|
1060
|
+
if (deps.state) {
|
|
1061
|
+
const realRunResult = await checkRealRunHealth(deps.state, date);
|
|
1062
|
+
if (realRunResult.ok) {
|
|
1063
|
+
digest.realRunHealth = {
|
|
1064
|
+
gatePassed: realRunResult.gate.gatePassed,
|
|
1065
|
+
contractSmokeOnly: realRunResult.gate.contractSmokeOnly,
|
|
1066
|
+
seededStateDetected: realRunResult.gate.seededStateDetected,
|
|
1067
|
+
hasRealClosure: realRunResult.gate.hasRealClosure,
|
|
1068
|
+
hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
|
|
1069
|
+
hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
|
|
1070
|
+
missingStage: realRunResult.gate.missingStage,
|
|
1071
|
+
missingReason: realRunResult.gate.missingReason,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
digest.realRunHealth = {
|
|
1076
|
+
gatePassed: false,
|
|
1077
|
+
contractSmokeOnly: false,
|
|
1078
|
+
seededStateDetected: false,
|
|
1079
|
+
hasRealClosure: false,
|
|
1080
|
+
hasQuietArtifact: false,
|
|
1081
|
+
hasDreamArtifact: false,
|
|
1082
|
+
missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1054
1086
|
const envelope = {
|
|
1055
1087
|
ok: true,
|
|
1056
1088
|
command: "heartbeat_digest",
|
|
@@ -1470,14 +1502,12 @@ export function createOpsRouter(deps) {
|
|
|
1470
1502
|
return envelope;
|
|
1471
1503
|
}
|
|
1472
1504
|
}
|
|
1473
|
-
// ─── T-V7C.C.4R: guidance_payload
|
|
1505
|
+
// ─── T-V7C.C.4R + T-GVS.R.1: guidance_payload ─────────────────────────
|
|
1474
1506
|
// Returns the assembled impulse + atmosphere for a given scene context.
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1507
|
+
// When state DB is wired, reads persisted artifact first; falls back to
|
|
1508
|
+
// real-time assembly and persists for subsequent reads.
|
|
1477
1509
|
if (command === "guidance_payload") {
|
|
1478
1510
|
const generatedAt = new Date().toISOString();
|
|
1479
|
-
const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
|
|
1480
|
-
const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1481
1511
|
const sceneType = input?.sceneType ?? "social";
|
|
1482
1512
|
const capabilityIntent = typeof input?.capabilityIntent === "string"
|
|
1483
1513
|
? input.capabilityIntent
|
|
@@ -1503,22 +1533,53 @@ export function createOpsRouter(deps) {
|
|
|
1503
1533
|
};
|
|
1504
1534
|
return envelope;
|
|
1505
1535
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1536
|
+
// T-GVS.R.1: Try reading persisted artifact first
|
|
1537
|
+
let artifactData;
|
|
1538
|
+
let warnings = [];
|
|
1539
|
+
let sourceRefs = [
|
|
1540
|
+
"guidance/capability-class.ts",
|
|
1541
|
+
"guidance/impulse-assembler.ts",
|
|
1542
|
+
"guidance/template-registry.ts",
|
|
1543
|
+
"guidance/output-guard.ts",
|
|
1544
|
+
];
|
|
1545
|
+
if (deps.state) {
|
|
1546
|
+
try {
|
|
1547
|
+
const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
|
|
1548
|
+
const existing = await readImpulseContext(deps.state, sceneType, capabilityIntent, platformId);
|
|
1549
|
+
if (existing.available) {
|
|
1550
|
+
artifactData = {
|
|
1551
|
+
sceneType: existing.artifact.sceneType,
|
|
1552
|
+
capabilityIntent: existing.artifact.capabilityIntent,
|
|
1553
|
+
platformId: existing.artifact.platformId,
|
|
1554
|
+
capabilityClass: existing.artifact.capabilityClass,
|
|
1555
|
+
impulseSource: existing.artifact.impulseSource,
|
|
1556
|
+
impulseText: existing.artifact.impulseText,
|
|
1557
|
+
atmosphereText: existing.artifact.atmosphereText,
|
|
1558
|
+
expressionBoundaryConstraints: existing.artifact.expressionBoundaryConstraints,
|
|
1559
|
+
expressionBoundaryStyle: existing.artifact.expressionBoundaryStyle,
|
|
1560
|
+
freshnessMs: existing.freshnessMs,
|
|
1561
|
+
persisted: true,
|
|
1562
|
+
};
|
|
1563
|
+
sourceRefs.push("core/second-nature/guidance/impulse-context-reader.ts");
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
catch {
|
|
1567
|
+
// Reader failure → fall through to assembly
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// Real-time assembly if no persisted artifact
|
|
1571
|
+
if (!artifactData) {
|
|
1572
|
+
const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
|
|
1573
|
+
const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
|
|
1574
|
+
const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1575
|
+
const impulseResult = assembleImpulseSync({
|
|
1576
|
+
sceneType: sceneType,
|
|
1577
|
+
capabilityIntent,
|
|
1578
|
+
platformId,
|
|
1579
|
+
});
|
|
1580
|
+
const atmosphere = getShortAtmosphereTemplate("active", "low");
|
|
1581
|
+
const expressionBoundary = buildExpressionBoundary(sceneType);
|
|
1582
|
+
artifactData = {
|
|
1522
1583
|
sceneType,
|
|
1523
1584
|
capabilityIntent: capabilityIntent ?? null,
|
|
1524
1585
|
platformId: platformId ?? null,
|
|
@@ -1530,16 +1591,41 @@ export function createOpsRouter(deps) {
|
|
|
1530
1591
|
atmosphereReviewStatus: atmosphere.reviewStatus,
|
|
1531
1592
|
expressionBoundaryConstraints: expressionBoundary.constraints,
|
|
1532
1593
|
expressionBoundaryStyle: expressionBoundary.style,
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1594
|
+
persisted: false,
|
|
1595
|
+
};
|
|
1596
|
+
if (impulseResult.source === "none") {
|
|
1597
|
+
warnings.push("no_impulse_available_for_this_scene_and_capability");
|
|
1598
|
+
}
|
|
1599
|
+
// T-GVS.R.1: Persist assembled artifact for future reads
|
|
1600
|
+
if (deps.state) {
|
|
1601
|
+
try {
|
|
1602
|
+
const { writeImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-writer.js");
|
|
1603
|
+
await writeImpulseContext(deps.state, {
|
|
1604
|
+
sceneType,
|
|
1605
|
+
capabilityIntent,
|
|
1606
|
+
platformId,
|
|
1607
|
+
impulseResult,
|
|
1608
|
+
atmosphereText: atmosphere.text,
|
|
1609
|
+
expressionBoundaryConstraints: expressionBoundary.constraints,
|
|
1610
|
+
expressionBoundaryStyle: expressionBoundary.style,
|
|
1611
|
+
}, { now: generatedAt });
|
|
1612
|
+
sourceRefs.push("core/second-nature/guidance/impulse-context-writer.ts");
|
|
1613
|
+
}
|
|
1614
|
+
catch {
|
|
1615
|
+
// Persistence failure is non-fatal; surface still returns assembled payload
|
|
1616
|
+
warnings.push("impulse_context_persistence_failed");
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
const envelope = {
|
|
1621
|
+
ok: true,
|
|
1622
|
+
command: "guidance_payload",
|
|
1623
|
+
runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
|
|
1624
|
+
surfaceMode: "cli",
|
|
1625
|
+
generatedAt,
|
|
1626
|
+
data: artifactData,
|
|
1627
|
+
warnings,
|
|
1628
|
+
sourceRefs,
|
|
1543
1629
|
};
|
|
1544
1630
|
return envelope;
|
|
1545
1631
|
}
|
|
@@ -26,6 +26,7 @@ import { type HeartbeatDigestAssemblerDeps } from "../../observability/services/
|
|
|
26
26
|
import type { GoalLifecyclePolicy } from "../../core/second-nature/heartbeat/goal-lifecycle-policy.js";
|
|
27
27
|
import type { IdleCuriosityPolicy } from "../../core/second-nature/heartbeat/idle-curiosity-policy.js";
|
|
28
28
|
import type { CircuitBreakerManager } from "../../core/second-nature/body/circuit-breaker/circuit-breaker-manager.js";
|
|
29
|
+
import type { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
|
|
29
30
|
export interface WorkspaceHeartbeatRunnerOptions {
|
|
30
31
|
/** When supplied, the runner persists the cycle so `loadStatus` can read it (T1.2.3). */
|
|
31
32
|
runtimeRecorder?: RuntimeDecisionRecorder;
|
|
@@ -76,6 +77,8 @@ export interface WorkspaceHeartbeatRunnerOptions {
|
|
|
76
77
|
*/
|
|
77
78
|
digestWindowHour?: number;
|
|
78
79
|
};
|
|
80
|
+
/** T-OBS.R.1: shared audit sink for heartbeat connector and Quiet outcomes. */
|
|
81
|
+
auditStore?: AppendOnlyAuditStore;
|
|
79
82
|
}
|
|
80
83
|
export declare function loadSnapshotInputsForWorkspaceHeartbeat(readModels: CliReadModels, options?: {
|
|
81
84
|
state?: StateDatabase;
|
|
@@ -145,6 +145,7 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
|
145
145
|
workspaceRoot: options.workspaceRoot,
|
|
146
146
|
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
147
147
|
dreamSchedulePort: options.dreamSchedulePort,
|
|
148
|
+
auditStore: options.auditStore,
|
|
148
149
|
}
|
|
149
150
|
: undefined,
|
|
150
151
|
connectorExecutor: options.connectorExecutor,
|
|
@@ -162,6 +163,7 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
|
162
163
|
idleCuriosityPolicy: options.idleCuriosityPolicy,
|
|
163
164
|
// v7 T-BTS.C.5: pass circuit breaker manager for execution health tracking.
|
|
164
165
|
circuitBreakerManager: options.circuitBreakerManager,
|
|
166
|
+
auditStore: options.auditStore,
|
|
165
167
|
},
|
|
166
168
|
});
|
|
167
169
|
if (options.runtimeRecorder) {
|
|
@@ -12,6 +12,14 @@ export interface ConnectorRequestIdentity {
|
|
|
12
12
|
/** Canonical name across all platforms. */
|
|
13
13
|
canonicalName?: string;
|
|
14
14
|
}
|
|
15
|
+
export interface PolicyProof {
|
|
16
|
+
decisionId: string;
|
|
17
|
+
decision: "allow" | "defer" | "downgrade" | "deny";
|
|
18
|
+
ownerConfirmMode?: boolean;
|
|
19
|
+
ownerConfirmed?: boolean;
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
15
23
|
export interface ConnectorRequest {
|
|
16
24
|
platformId: string;
|
|
17
25
|
intent: CapabilityIntent;
|
|
@@ -23,6 +31,8 @@ export interface ConnectorRequest {
|
|
|
23
31
|
intentId?: string;
|
|
24
32
|
/** T-V7C.C.4: identity for connector request (readable, no credential). */
|
|
25
33
|
identity?: ConnectorRequestIdentity;
|
|
34
|
+
/** T-CS.R.1: policy proof for write-side actions */
|
|
35
|
+
policyProof?: PolicyProof;
|
|
26
36
|
}
|
|
27
37
|
export interface ExecutionPlan {
|
|
28
38
|
platformId: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolicyBoundWriteDispatch — Write-side connector dispatch with policy proof gate.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Verify policy proof before external write; reject without platform
|
|
5
|
+
* call when proof is missing, deny, or lacks owner confirmation for high-risk
|
|
6
|
+
* actions. Support dry-run mode for safe testing.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §2`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
|
|
11
|
+
* - `docs/validation/openclaw-plugin-classification.md §5`
|
|
12
|
+
*
|
|
13
|
+
* Dependencies:
|
|
14
|
+
* - `src/connectors/base/contract.js` (ConnectorRequest, ConnectorResult, PolicyProof)
|
|
15
|
+
* - `src/connectors/base/policy-layer.js` (createConnectorPolicyLayer)
|
|
16
|
+
*
|
|
17
|
+
* Boundary:
|
|
18
|
+
* - Does NOT bypass ActionPolicyDecision.
|
|
19
|
+
* - Does NOT execute write without valid policy proof.
|
|
20
|
+
* - Does NOT leak credentials in returned results.
|
|
21
|
+
*/
|
|
22
|
+
import type { ConnectorRequest, ConnectorResult } from "./contract.js";
|
|
23
|
+
export interface WriteDispatchResult {
|
|
24
|
+
status: "allowed" | "denied" | "deferred" | "dry_run" | "downgraded";
|
|
25
|
+
reason: string;
|
|
26
|
+
connectorResult?: ConnectorResult<unknown>;
|
|
27
|
+
simulatedPayload?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export declare function dispatchPolicyBoundWrite(request: ConnectorRequest, executeConnector: (req: ConnectorRequest) => Promise<ConnectorResult<unknown>>): Promise<WriteDispatchResult>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolicyBoundWriteDispatch — Write-side connector dispatch with policy proof gate.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Verify policy proof before external write; reject without platform
|
|
5
|
+
* call when proof is missing, deny, or lacks owner confirmation for high-risk
|
|
6
|
+
* actions. Support dry-run mode for safe testing.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §2`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
|
|
11
|
+
* - `docs/validation/openclaw-plugin-classification.md §5`
|
|
12
|
+
*
|
|
13
|
+
* Dependencies:
|
|
14
|
+
* - `src/connectors/base/contract.js` (ConnectorRequest, ConnectorResult, PolicyProof)
|
|
15
|
+
* - `src/connectors/base/policy-layer.js` (createConnectorPolicyLayer)
|
|
16
|
+
*
|
|
17
|
+
* Boundary:
|
|
18
|
+
* - Does NOT bypass ActionPolicyDecision.
|
|
19
|
+
* - Does NOT execute write without valid policy proof.
|
|
20
|
+
* - Does NOT leak credentials in returned results.
|
|
21
|
+
*/
|
|
22
|
+
// ───────────────────────────────────────────────────────────────
|
|
23
|
+
// Helpers
|
|
24
|
+
// ───────────────────────────────────────────────────────────────
|
|
25
|
+
function isWriteCapability(intent) {
|
|
26
|
+
const writeIntents = ["post.publish", "comment.reply", "message.send"];
|
|
27
|
+
return writeIntents.includes(intent);
|
|
28
|
+
}
|
|
29
|
+
function validatePolicyProof(proof, intent) {
|
|
30
|
+
if (!proof) {
|
|
31
|
+
return {
|
|
32
|
+
valid: false,
|
|
33
|
+
reason: "policy_denied_missing_permission",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (proof.decision === "deny") {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
reason: proof.reason || "policy_denied_high_risk",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (proof.decision === "defer") {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
reason: "policy_deferred_owner_confirmation",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (proof.decision === "downgrade") {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
reason: proof.reason || "policy_downgraded_to_draft",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Allow decision
|
|
55
|
+
if (proof.decision !== "allow") {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
reason: "policy_denied_missing_permission",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// For write capabilities, require owner-confirm, dry-run, or explicit owner-confirmed flag
|
|
62
|
+
if (isWriteCapability(intent) &&
|
|
63
|
+
!proof.ownerConfirmMode &&
|
|
64
|
+
!proof.dryRun &&
|
|
65
|
+
!proof.ownerConfirmed) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
reason: "policy_denied_owner_confirm_required",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { valid: true, reason: "policy_allowed" };
|
|
72
|
+
}
|
|
73
|
+
// ───────────────────────────────────────────────────────────────
|
|
74
|
+
// Public API
|
|
75
|
+
// ───────────────────────────────────────────────────────────────
|
|
76
|
+
export async function dispatchPolicyBoundWrite(request, executeConnector) {
|
|
77
|
+
const { intent, policyProof, payload } = request;
|
|
78
|
+
// Only gate write capabilities
|
|
79
|
+
if (!isWriteCapability(intent)) {
|
|
80
|
+
// Read capabilities pass through to normal execution
|
|
81
|
+
const result = await executeConnector(request);
|
|
82
|
+
return {
|
|
83
|
+
status: "allowed",
|
|
84
|
+
reason: "read_capability_no_policy_gate",
|
|
85
|
+
connectorResult: result,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Validate policy proof
|
|
89
|
+
const validation = validatePolicyProof(policyProof, intent);
|
|
90
|
+
if (!validation.valid) {
|
|
91
|
+
return {
|
|
92
|
+
status: validation.reason === "policy_deferred_owner_confirmation"
|
|
93
|
+
? "deferred"
|
|
94
|
+
: "denied",
|
|
95
|
+
reason: validation.reason,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Dry-run mode: simulate execution without platform call
|
|
99
|
+
if (policyProof?.dryRun) {
|
|
100
|
+
return {
|
|
101
|
+
status: "dry_run",
|
|
102
|
+
reason: "dry_run_simulated_success",
|
|
103
|
+
simulatedPayload: {
|
|
104
|
+
...payload,
|
|
105
|
+
_simulated: true,
|
|
106
|
+
_idempotencyKey: request.idempotencyKey,
|
|
107
|
+
_decisionId: policyProof.decisionId,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Owner-confirm mode without explicit approval: defer to owner approval
|
|
112
|
+
if (policyProof?.ownerConfirmMode && !policyProof?.ownerConfirmed) {
|
|
113
|
+
return {
|
|
114
|
+
status: "deferred",
|
|
115
|
+
reason: "owner_confirm_pending",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Full allow → execute connector
|
|
119
|
+
const result = await executeConnector(request);
|
|
120
|
+
return {
|
|
121
|
+
status: result.status === "success" ? "allowed" : "denied",
|
|
122
|
+
reason: result.status === "success"
|
|
123
|
+
? "execution_completed"
|
|
124
|
+
: (result.failureClass || "execution_failed"),
|
|
125
|
+
connectorResult: result,
|
|
126
|
+
};
|
|
127
|
+
}
|