@principles/host-runtime 0.1.0 → 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/dist/active-principle-prompt.js +5 -1
- package/dist/governance-observation-store.d.ts +196 -0
- package/dist/governance-observation-store.js +696 -0
- package/dist/governance-signal-admission.d.ts +228 -0
- package/dist/governance-signal-admission.js +853 -0
- package/dist/host-liveness-contract.d.ts +3 -0
- package/dist/host-liveness-contract.js +18 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +17 -0
- package/dist/internalization-consumer-cycle.d.ts +43 -0
- package/dist/internalization-consumer-cycle.js +468 -0
- package/dist/internalization-consumer-governance.d.ts +39 -0
- package/dist/internalization-consumer-governance.js +152 -0
- package/dist/legacy-rule-contract-symbols.d.ts +20 -0
- package/dist/legacy-rule-contract-symbols.js +124 -0
- package/dist/product-telemetry/consent-store.d.ts +107 -0
- package/dist/product-telemetry/consent-store.js +329 -0
- package/dist/product-telemetry/eligibility.d.ts +53 -0
- package/dist/product-telemetry/eligibility.js +69 -0
- package/dist/product-telemetry/exporter.d.ts +46 -0
- package/dist/product-telemetry/exporter.js +71 -0
- package/dist/product-telemetry/milestone-readers.d.ts +61 -0
- package/dist/product-telemetry/milestone-readers.js +234 -0
- package/dist/product-telemetry/service.d.ts +142 -0
- package/dist/product-telemetry/service.js +532 -0
- package/dist/product-telemetry/workspace-scope.d.ts +38 -0
- package/dist/product-telemetry/workspace-scope.js +64 -0
- package/dist/production-pain-evidence.d.ts +60 -0
- package/dist/production-pain-evidence.js +82 -12
- package/dist/production-rulehost-gate.d.ts +0 -1
- package/dist/production-rulehost-gate.js +47 -6
- package/dist/rule-implementation-runtime.js +0 -2
- package/dist/workspace-telemetry-emitter.d.ts +15 -0
- package/dist/workspace-telemetry-emitter.js +74 -0
- package/package.json +7 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internalization consumer governance wiring (host-neutral, PRI-624).
|
|
3
|
+
*
|
|
4
|
+
* Ported verbatim from openclaw-plugin `auto-consumer-governance-wiring.ts`
|
|
5
|
+
* (P0-D/E/F 生产接线) so the OpenClaw auto-consumer and the Companion
|
|
6
|
+
* workspace worker share ONE wiring implementation instead of copying it:
|
|
7
|
+
* - evaluator: isRepairLoopEnabled + seedArtificerRepairTask (bounded repair)
|
|
8
|
+
* - rollout_reviewer: dispatchActivation (approve_rollout → ActivationDispatcher,
|
|
9
|
+
* 低风险 auto_activate / 高风险 approvals.pending) + reopenRevisionTarget
|
|
10
|
+
* (needs_revision → reopen scribe/artificer, 禁止入 approval)
|
|
11
|
+
*
|
|
12
|
+
* Everything is assembled from @principles/core stores/dispatchers; the only
|
|
13
|
+
* host knowledge is a structural logger. Idempotency (INV-08): dispatcher
|
|
14
|
+
* dedupes by `${artifactId}::${channel}`; reopen idempotency is owned by
|
|
15
|
+
* orchestrator.reopenTaskForRevision.
|
|
16
|
+
*/
|
|
17
|
+
import { ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, SqliteConnection, SqliteActivationStateStore, SqliteApprovalQueueStore, SqlitePIArtifactStore, createProductionGateDeps, createPITaskDiagnosticJson, computeFeatureFlagsFromConfig, isFeatureEnabled, } from '@principles/core/runtime-v2';
|
|
18
|
+
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
19
|
+
function normalizeDecision(decision) {
|
|
20
|
+
if (decision.decision === 'activated') {
|
|
21
|
+
return { decision: decision.decision, activationId: decision.activationId };
|
|
22
|
+
}
|
|
23
|
+
if (decision.decision === 'already_activated') {
|
|
24
|
+
return { decision: decision.decision, reason: 'idempotent_redispatch' };
|
|
25
|
+
}
|
|
26
|
+
if (decision.decision === 'queued_for_approval') {
|
|
27
|
+
return { decision: decision.decision, reason: decision.approvalId };
|
|
28
|
+
}
|
|
29
|
+
// refused / would_* 家族
|
|
30
|
+
const reason = 'reason' in decision && typeof decision.reason === 'string' ? decision.reason : decision.decision;
|
|
31
|
+
return { decision: decision.decision, reason };
|
|
32
|
+
}
|
|
33
|
+
function makeFlagProbe(workspaceDir) {
|
|
34
|
+
const configResult = loadPdConfigForPlugin(workspaceDir);
|
|
35
|
+
if (!configResult.ok)
|
|
36
|
+
return () => false;
|
|
37
|
+
const flags = computeFeatureFlagsFromConfig(configResult.effective);
|
|
38
|
+
return (flagId) => isFeatureEnabled(flags, flagId);
|
|
39
|
+
}
|
|
40
|
+
/** rollout → ActivationDispatcher 的生产接线 (per dispatch 打开短连接, 与 Console 模式一致) */
|
|
41
|
+
export async function dispatchRolloutActivation(workspaceDir, input, logger) {
|
|
42
|
+
const connection = new SqliteConnection(workspaceDir);
|
|
43
|
+
try {
|
|
44
|
+
const piArtifactStore = new SqlitePIArtifactStore(connection);
|
|
45
|
+
const artifactReadModel = {
|
|
46
|
+
getArtifactById: async (id) => {
|
|
47
|
+
const rec = await piArtifactStore.getArtifactById(id);
|
|
48
|
+
if (!rec)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
artifactId: rec.artifactId,
|
|
52
|
+
artifactKind: rec.artifactKind,
|
|
53
|
+
sourceTaskId: rec.sourceTaskId,
|
|
54
|
+
sourcePrincipleId: rec.sourcePrincipleId,
|
|
55
|
+
sourceRuleId: rec.sourceRuleId,
|
|
56
|
+
lineageArtifactIds: rec.lineageArtifactIds,
|
|
57
|
+
validationStatus: rec.validationStatus,
|
|
58
|
+
contentJson: rec.contentJson,
|
|
59
|
+
createdAt: rec.createdAt,
|
|
60
|
+
updatedAt: rec.updatedAt,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const activationStateStore = new SqliteActivationStateStore(connection);
|
|
65
|
+
const approvalQueueStore = new SqliteApprovalQueueStore(connection);
|
|
66
|
+
const flagProbe = makeFlagProbe(workspaceDir);
|
|
67
|
+
const dispatcher = new ActivationDispatcher(artifactReadModel, activationStateStore, {
|
|
68
|
+
writers: [
|
|
69
|
+
new PromptWriter(),
|
|
70
|
+
new RuleHostWriter({ gateDeps: createProductionGateDeps(), featureFlagProbe: flagProbe }),
|
|
71
|
+
new DeferArchiveWriter(),
|
|
72
|
+
],
|
|
73
|
+
approvalQueueStore,
|
|
74
|
+
});
|
|
75
|
+
const decision = await dispatcher.dispatch({
|
|
76
|
+
artifactId: input.artifactId,
|
|
77
|
+
channel: input.channel, // InternalizationChannel union; rollout 链的 channel 已由任务元数据校验
|
|
78
|
+
rolloutDecision: 'auto_activate',
|
|
79
|
+
actor: { kind: 'system', source: 'rollout_reviewer' },
|
|
80
|
+
now: new Date().toISOString(),
|
|
81
|
+
confirm: true,
|
|
82
|
+
confidence: input.confidence,
|
|
83
|
+
});
|
|
84
|
+
const outcome = normalizeDecision(decision);
|
|
85
|
+
logger?.info?.(`[PD:Consumer] rollout dispatch: artifact=${input.artifactId} channel=${input.channel} → ${outcome.decision}${outcome.activationId ? ` (${outcome.activationId})` : ''}${outcome.reason ? ` reason=${outcome.reason}` : ''}`);
|
|
86
|
+
return outcome;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
try {
|
|
90
|
+
connection.close();
|
|
91
|
+
}
|
|
92
|
+
catch { /* best-effort */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** flag 探针: config 异常 → 全 false (fail-closed, 不 throw) */
|
|
96
|
+
/** evaluator repair deps (PRI-509 机制的生产接线; flag 语义保留为运行时开关) */
|
|
97
|
+
export function createEvaluatorRepairDeps(workspaceDir, stateManager, logger) {
|
|
98
|
+
return {
|
|
99
|
+
isRepairLoopEnabled: () => {
|
|
100
|
+
// flag evaluator_artificer_repair_loop (registry 默认已改为 ON — 见
|
|
101
|
+
// feature-flag-contract.ts;此处读取 config 以保留运行时可关闭能力)
|
|
102
|
+
const configResult = loadPdConfigForPlugin(workspaceDir);
|
|
103
|
+
if (!configResult.ok)
|
|
104
|
+
return false;
|
|
105
|
+
const flags = computeFeatureFlagsFromConfig(configResult.effective);
|
|
106
|
+
return isFeatureEnabled(flags, 'evaluator_artificer_repair_loop');
|
|
107
|
+
},
|
|
108
|
+
seedArtificerRepairTask: async (params) => {
|
|
109
|
+
// P0-4: 确定性 revision identity — evaluatorTaskId + iteration 唯一定位
|
|
110
|
+
// 一个逻辑 repair 任务; 重放 (consumer 重复周期 / crash 恢复) reuse 而非再建。
|
|
111
|
+
const repairTaskId = `artificer-repair-${params.repairPayload.sourceEvaluatorTaskId}-r${params.repairPayload.repairIteration}`;
|
|
112
|
+
const existing = await stateManager.getTask(repairTaskId);
|
|
113
|
+
if (existing) {
|
|
114
|
+
logger?.info?.(`[PD:Consumer] repair task ${repairTaskId} already exists; reusing (idempotent seed)`);
|
|
115
|
+
return repairTaskId;
|
|
116
|
+
}
|
|
117
|
+
await stateManager.createTask({
|
|
118
|
+
taskId: repairTaskId,
|
|
119
|
+
taskKind: 'artificer',
|
|
120
|
+
status: 'pending',
|
|
121
|
+
attemptCount: 0,
|
|
122
|
+
maxAttempts: 3,
|
|
123
|
+
diagnosticJson: createPITaskDiagnosticJson({
|
|
124
|
+
dependencyTaskIds: [...params.inheritedDependencyTaskIds],
|
|
125
|
+
channel: params.inheritedChannel,
|
|
126
|
+
timeoutMs: params.inheritedTimeoutMs,
|
|
127
|
+
inputArtifactRefs: [...params.inheritedInputArtifactRefs],
|
|
128
|
+
outputArtifactRefs: [],
|
|
129
|
+
repairPayload: params.repairPayload,
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
logger?.info?.(`[PD:Consumer] seeded artificer repair task ${repairTaskId} (iteration ${params.repairPayload.repairIteration})`);
|
|
133
|
+
return repairTaskId;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** rollout reviewer 治理 deps: dispatch + revision reopen */
|
|
138
|
+
export function createRolloutGovernanceDeps(workspaceDir, orchestrator, logger) {
|
|
139
|
+
return {
|
|
140
|
+
dispatchActivation: (input) => dispatchRolloutActivation(workspaceDir, input, logger),
|
|
141
|
+
reopenRevisionTarget: async (input) => {
|
|
142
|
+
const result = await orchestrator.reopenTaskForRevision(input.targetTaskId, {
|
|
143
|
+
revisionFeedback: input.revisionFeedback,
|
|
144
|
+
reason: `rollout_revision_iteration_${input.revisionIteration}`,
|
|
145
|
+
revisionCauseId: `rollout-${input.sourceRolloutTaskId}-r${input.revisionIteration}`,
|
|
146
|
+
});
|
|
147
|
+
return result.ok
|
|
148
|
+
? { ok: true, reason: result.reason, reopenedTaskId: input.targetTaskId }
|
|
149
|
+
: { ok: false, reason: result.reason };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retired RuleHost contract symbol scan — host-runtime local copy.
|
|
3
|
+
*
|
|
4
|
+
* The shared canonical scanner lives in @principles/core
|
|
5
|
+
* (internalization/legacy-rule-contract-scanner.ts) and is used by the
|
|
6
|
+
* plugin, console, installer, and pd-cli. This module deliberately does NOT
|
|
7
|
+
* import it: the published codex-adapter bundle installs host-runtime
|
|
8
|
+
* against the CURRENTLY PUBLISHED @principles/core from the npm registry,
|
|
9
|
+
* and a new runtime import would break clean installs until the next core
|
|
10
|
+
* release (see tests/published-codex-adapter-bundle.test.ts — it enforces
|
|
11
|
+
* exactly this constraint). Keep the symbol patterns in sync with the core
|
|
12
|
+
* scanner; the host-runtime tests cover the same detection behavior.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Conservative scan of persisted RuleCode source for retired RuleHost
|
|
16
|
+
* contract symbols (field reads and helper calls). A hit means the rule must
|
|
17
|
+
* NOT execute against the current contract — its reads would silently
|
|
18
|
+
* resolve to undefined and change owner-approved behavior.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scanRetiredContractSymbols(implementationCode: string): string[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retired RuleHost contract symbol scan — host-runtime local copy.
|
|
3
|
+
*
|
|
4
|
+
* The shared canonical scanner lives in @principles/core
|
|
5
|
+
* (internalization/legacy-rule-contract-scanner.ts) and is used by the
|
|
6
|
+
* plugin, console, installer, and pd-cli. This module deliberately does NOT
|
|
7
|
+
* import it: the published codex-adapter bundle installs host-runtime
|
|
8
|
+
* against the CURRENTLY PUBLISHED @principles/core from the npm registry,
|
|
9
|
+
* and a new runtime import would break clean installs until the next core
|
|
10
|
+
* release (see tests/published-codex-adapter-bundle.test.ts — it enforces
|
|
11
|
+
* exactly this constraint). Keep the symbol patterns in sync with the core
|
|
12
|
+
* scanner; the host-runtime tests cover the same detection behavior.
|
|
13
|
+
*/
|
|
14
|
+
// Helper-call form is listed before the bare-word form; the field pattern's
|
|
15
|
+
// negative lookahead prevents one hasPlanFile( call reporting twice.
|
|
16
|
+
const RETIRED_SYMBOL_PATTERNS = [
|
|
17
|
+
{ symbol: 'recentThinking', pattern: /\brecentThinking\b/ },
|
|
18
|
+
{ symbol: 'planStatus', pattern: /\bplanStatus\b/ },
|
|
19
|
+
{ symbol: 'getPlanStatus', pattern: /\bgetPlanStatus\s*\(/ },
|
|
20
|
+
{ symbol: 'hasPlanFileHelper', pattern: /\bhasPlanFile\s*\(/ },
|
|
21
|
+
{ symbol: 'hasPlanFile', pattern: /\bhasPlanFile\b(?!\s*\()/ },
|
|
22
|
+
];
|
|
23
|
+
function maskNonExecutableText(source) {
|
|
24
|
+
let state = 'code';
|
|
25
|
+
let templateExpressionDepth = 0;
|
|
26
|
+
let masked = '';
|
|
27
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
28
|
+
const char = source[index] ?? '';
|
|
29
|
+
const next = source[index + 1] ?? '';
|
|
30
|
+
if (state === 'code') {
|
|
31
|
+
if (templateExpressionDepth > 0 && char === '{') {
|
|
32
|
+
templateExpressionDepth += 1;
|
|
33
|
+
masked += char;
|
|
34
|
+
}
|
|
35
|
+
else if (templateExpressionDepth > 0 && char === '}') {
|
|
36
|
+
templateExpressionDepth -= 1;
|
|
37
|
+
masked += char;
|
|
38
|
+
if (templateExpressionDepth === 0)
|
|
39
|
+
state = 'template';
|
|
40
|
+
}
|
|
41
|
+
else if (char === '/' && next === '/') {
|
|
42
|
+
masked += ' ';
|
|
43
|
+
index += 1;
|
|
44
|
+
state = 'line_comment';
|
|
45
|
+
}
|
|
46
|
+
else if (char === '/' && next === '*') {
|
|
47
|
+
masked += ' ';
|
|
48
|
+
index += 1;
|
|
49
|
+
state = 'block_comment';
|
|
50
|
+
}
|
|
51
|
+
else if (char === "'") {
|
|
52
|
+
masked += ' ';
|
|
53
|
+
state = 'single_quote';
|
|
54
|
+
}
|
|
55
|
+
else if (char === '"') {
|
|
56
|
+
masked += ' ';
|
|
57
|
+
state = 'double_quote';
|
|
58
|
+
}
|
|
59
|
+
else if (char === '`') {
|
|
60
|
+
masked += ' ';
|
|
61
|
+
state = 'template';
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
masked += char;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (state === 'line_comment') {
|
|
69
|
+
masked += char === '\n' || char === '\r' ? char : ' ';
|
|
70
|
+
if (char === '\n' || char === '\r')
|
|
71
|
+
state = 'code';
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (state === 'block_comment') {
|
|
75
|
+
if (char === '*' && next === '/') {
|
|
76
|
+
masked += ' ';
|
|
77
|
+
index += 1;
|
|
78
|
+
state = 'code';
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
masked += char === '\n' || char === '\r' ? char : ' ';
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (state === 'template' && char === '$' && next === '{') {
|
|
86
|
+
masked += ' {';
|
|
87
|
+
index += 1;
|
|
88
|
+
templateExpressionDepth = 1;
|
|
89
|
+
state = 'code';
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === '\\') {
|
|
93
|
+
masked += ' ';
|
|
94
|
+
if (index + 1 < source.length) {
|
|
95
|
+
masked += source[index + 1] === '\n' || source[index + 1] === '\r' ? source[index + 1] : ' ';
|
|
96
|
+
index += 1;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const closesLiteral = (state === 'single_quote' && char === "'") ||
|
|
101
|
+
(state === 'double_quote' && char === '"') ||
|
|
102
|
+
(state === 'template' && char === '`');
|
|
103
|
+
masked += char === '\n' || char === '\r' ? char : ' ';
|
|
104
|
+
if (closesLiteral)
|
|
105
|
+
state = 'code';
|
|
106
|
+
}
|
|
107
|
+
return masked;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Conservative scan of persisted RuleCode source for retired RuleHost
|
|
111
|
+
* contract symbols (field reads and helper calls). A hit means the rule must
|
|
112
|
+
* NOT execute against the current contract — its reads would silently
|
|
113
|
+
* resolve to undefined and change owner-approved behavior.
|
|
114
|
+
*/
|
|
115
|
+
export function scanRetiredContractSymbols(implementationCode) {
|
|
116
|
+
const executableSource = maskNonExecutableText(implementationCode);
|
|
117
|
+
const symbols = [];
|
|
118
|
+
for (const { symbol, pattern } of RETIRED_SYMBOL_PATTERNS) {
|
|
119
|
+
if (pattern.test(executableSource)) {
|
|
120
|
+
symbols.push(symbol);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return symbols;
|
|
124
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product telemetry control-state store — Anonymous Product Telemetry v1
|
|
3
|
+
* (PRI-597, SPEC §44; review remediation: workspace-scoped export state).
|
|
4
|
+
*
|
|
5
|
+
* Persists the telemetry control state to `~/.pd/product-telemetry.json`.
|
|
6
|
+
* This file is Telemetry Control State — it never enters
|
|
7
|
+
* Principle/Pain/receipt/governance stores, and the secret never leaves the
|
|
8
|
+
* machine.
|
|
9
|
+
*
|
|
10
|
+
* Scope model (schema v2, review remediation P1-1):
|
|
11
|
+
* - MACHINE scope: consent, consentVersion, telemetrySecret.
|
|
12
|
+
* - WORKSPACE scope: `workspaceExports[scopeId]` — per-workspace dedup,
|
|
13
|
+
* retry, and attempt bookkeeping keyed by the opaque local scope ID
|
|
14
|
+
* (HMAC(secret, canonical workspace path); never uploaded). One
|
|
15
|
+
* workspace succeeding must never suppress another workspace's export.
|
|
16
|
+
*
|
|
17
|
+
* Migration from schema v1: consent, consentVersion, and telemetrySecret are
|
|
18
|
+
* preserved; legacy machine-global export bookkeeping (lastSucceededAt,
|
|
19
|
+
* retry state, attempt counters) is DISCARDED — it cannot be attributed to a
|
|
20
|
+
* workspace, and it is operational state, not a governance fact. Dropping it
|
|
21
|
+
* can cause at most one extra same-day snapshot per installation.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PRODUCT_TELEMETRY_STATE_FILENAME = "product-telemetry.json";
|
|
24
|
+
export declare const PRODUCT_TELEMETRY_CONTROL_SCHEMA_VERSION = "2";
|
|
25
|
+
/** v1 files are still read (then migrated in memory and rewritten as v2). */
|
|
26
|
+
export declare const PRODUCT_TELEMETRY_LEGACY_CONTROL_SCHEMA_VERSION = "1";
|
|
27
|
+
/** Hard bound on tracked workspaces so the file cannot grow unboundedly. */
|
|
28
|
+
export declare const MAX_WORKSPACE_EXPORT_ENTRIES = 200;
|
|
29
|
+
/**
|
|
30
|
+
* Entries untouched for this long are pruned on the next write. This is local
|
|
31
|
+
* telemetry operational state only — no workspace history is retained.
|
|
32
|
+
*/
|
|
33
|
+
export declare const WORKSPACE_EXPORT_STATE_MAX_AGE_DAYS = 30;
|
|
34
|
+
export type ProductTelemetryConsent = 'unset' | 'granted' | 'denied';
|
|
35
|
+
/** Per-workspace export bookkeeping (operational state; never exported). */
|
|
36
|
+
export interface WorkspaceExportState {
|
|
37
|
+
lastAttemptedAt?: string;
|
|
38
|
+
lastSucceededAt?: string;
|
|
39
|
+
/** Coarse failure code (TelemetryFailureCode). No response bodies. */
|
|
40
|
+
lastFailureCode?: string;
|
|
41
|
+
nextRetryAt?: string;
|
|
42
|
+
/** Failed export attempts in the current attemptBucketDate (0–99; hard cap enforced by the service). */
|
|
43
|
+
dailyAttemptCount?: number;
|
|
44
|
+
/** UTC date bucket the dailyAttemptCount belongs to (resets on day change). */
|
|
45
|
+
attemptBucketDate?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ProductTelemetryControlState {
|
|
48
|
+
consent: ProductTelemetryConsent;
|
|
49
|
+
consentVersion: string;
|
|
50
|
+
/** Cryptographically random hex secret. Never uploaded. */
|
|
51
|
+
telemetrySecret?: string;
|
|
52
|
+
/** Per-workspace export bookkeeping keyed by opaque local scope ID. */
|
|
53
|
+
workspaceExports?: Record<string, WorkspaceExportState>;
|
|
54
|
+
schemaVersion: string;
|
|
55
|
+
}
|
|
56
|
+
export type ControlStateRead = {
|
|
57
|
+
ok: true;
|
|
58
|
+
state: ProductTelemetryControlState;
|
|
59
|
+
existed: boolean;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
reason: string;
|
|
63
|
+
nextAction: string;
|
|
64
|
+
};
|
|
65
|
+
export type ControlStateWrite = {
|
|
66
|
+
ok: true;
|
|
67
|
+
} | {
|
|
68
|
+
ok: false;
|
|
69
|
+
reason: string;
|
|
70
|
+
nextAction: string;
|
|
71
|
+
};
|
|
72
|
+
export declare function defaultProductTelemetryControlState(): ProductTelemetryControlState;
|
|
73
|
+
export declare function getProductTelemetryStatePath(homeDir: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Read and validate the control state. A missing file is the normal
|
|
76
|
+
* never-configured case (defaults, existed=false). A malformed file is a
|
|
77
|
+
* loud failure — silently treating it as "unset" could re-prompt or re-export
|
|
78
|
+
* against the user's recorded decision (rc-3/rc-9).
|
|
79
|
+
*
|
|
80
|
+
* v1 files are migrated in memory (export bookkeeping dropped, consent
|
|
81
|
+
* identity preserved); the migration is persisted on the next write.
|
|
82
|
+
*/
|
|
83
|
+
export declare function readProductTelemetryControlState(homeDir: string): ControlStateRead;
|
|
84
|
+
/**
|
|
85
|
+
* Drop `workspaceExports` entries whose most recent activity (attempt or
|
|
86
|
+
* success) is older than WORKSPACE_EXPORT_STATE_MAX_AGE_DAYS. Bounded local
|
|
87
|
+
* operational state only — this is not a workspace history database.
|
|
88
|
+
*/
|
|
89
|
+
export declare function pruneWorkspaceExports(state: ProductTelemetryControlState, nowMs: number): ProductTelemetryControlState;
|
|
90
|
+
/** Atomic write (temp file + rename) so a crash never truncates the state. */
|
|
91
|
+
export declare function writeProductTelemetryControlState(homeDir: string, state: ProductTelemetryControlState): ControlStateWrite;
|
|
92
|
+
/** State after `pd telemetry enable`: explicit granted consent + secret. */
|
|
93
|
+
export declare function grantedControlState(previous: ProductTelemetryControlState): ProductTelemetryControlState;
|
|
94
|
+
/**
|
|
95
|
+
* State after `pd telemetry disable`: consent denied and all export identity
|
|
96
|
+
* removed (SPEC §19). The explicit `denied` choice is preserved so PD never
|
|
97
|
+
* re-prompts.
|
|
98
|
+
*/
|
|
99
|
+
export declare function deniedControlState(): ProductTelemetryControlState;
|
|
100
|
+
/**
|
|
101
|
+
* State after `pd telemetry reset` (SPEC §18): secret and export status are
|
|
102
|
+
* deleted — no future daily ID relates to previous ones. The consent choice
|
|
103
|
+
* is preserved; a fresh secret is generated only while telemetry remains
|
|
104
|
+
* enabled. Workspace bookkeeping keyed under the OLD secret's scope IDs is
|
|
105
|
+
* dropped with it (the IDs are meaningless under a new secret).
|
|
106
|
+
*/
|
|
107
|
+
export declare function resetControlState(previous: ProductTelemetryControlState): ProductTelemetryControlState;
|