@codedealer/pi-sync 1.0.0 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/cli.js +17 -3
package/README.md CHANGED
@@ -81,7 +81,7 @@ Resources use per-type roots, and every source and target must stay inside its d
81
81
  | `agent` | `resources/agents/NAME.md` | `agents/NAME.md` | single `.md` file |
82
82
  | `role` | `resources/roles/NAME.md` | `roles/NAME.md` | single `.md` file |
83
83
 
84
- For non-skill types the source and target must use the same name. Symbolic links are rejected, paths use relative POSIX syntax, and no path may traverse outside its root. `node_modules` directories are never imported or managed; only authored content and portable extension configuration belong in the setup repository. Package-installed code is never imported. The optional schema-v1 `settings` list supports explicitly selected JSON Pointer fields in `settings.json`, `models.json`, `keybindings.json`, or `mcp.json`; each entry has a `target` and `fields` array of `{ "path": "/nested/key", "value": ... }`. Keep paths narrow and never declare credentials or machine-local values; Pi Sync rejects credential-like field names as a defense-in-depth check. This is not a general secret scanner. Unselected keys and values are preserved. A pre-existing selected setting that differs from its declared value is reported as a collision on first apply rather than overwritten. Unsupported schema fields, resource types, malformed dependency declarations, and lockfile package entries that do not exactly match manifest dependencies are rejected before the local installation is changed.
84
+ For non-skill types the source and target must use the same name. Symbolic links are rejected, paths use relative POSIX syntax, and no path may traverse outside its root. `node_modules` directories are never imported or managed; only authored content and portable extension configuration belong in the setup repository. Package-installed code is never imported. The optional schema-v1 `settings` list supports explicitly selected JSON Pointer fields in `settings.json`, `models.json`, `keybindings.json`, or `mcp.json`; each entry has a `target` and `fields` array of `{ "path": "/nested/key", "value": ... }`. Keep paths narrow and never declare literal credentials or machine-local values; Pi Sync rejects credential-like field names in managed setting values as a defense-in-depth check, with one exception: a credential-like key may hold a strict `$NAME` or `${NAME}` environment reference (which Pi interpolates at request time), so the reference is synced while the actual value stays configured per machine. This is not a general secret scanner. Unselected keys and values are preserved. A pre-existing selected setting that differs from its declared value is reported as a collision on first apply rather than overwritten. Unsupported schema fields, resource types, malformed dependency declarations, and lockfile package entries that do not exactly match manifest dependencies are rejected before the local installation is changed.
85
85
 
86
86
  Apply stores its last-applied content baseline and recoverable backups under `<agent-dir>/.pi-sync/`, outside the setup repository. The baseline enables file-level three-way resource reconciliation and per-field settings reconciliation: repository-only changes are applied independently, local-only changes are preserved and reported as `local-divergence`, and overlapping changes are reported as conflicts without overwriting the conflicted item. Non-conflicting files and settings fields can still be reconciled in the same apply. Apply operations are serialized with a local lock; if a lock's owner process has exited, apply fails closed and asks the user to remove `.pi-sync/apply.lock` only after confirming that no apply process is running. Local state and backups are not part of the setup repository.
87
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codedealer/pi-sync",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A manifest-driven CLI for applying explicitly managed Pi resources.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -203,14 +203,28 @@ async function findLikelySecrets(root, relative) {
203
203
  return findings;
204
204
  }
205
205
 
206
+ const ENV_REFERENCE = /^\$[A-Za-z_][A-Za-z0-9_]*$|^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/;
207
+ // LLM usage/limit parameters that contain "token" but never carry credentials.
208
+ const NON_CREDENTIAL_TOKEN_KEYS = new Set(['tokens', 'maxtokens', 'mintokens', 'tokencount', 'tokenlimit', 'numtokens']);
209
+
210
+ function isCredentialLikeKey(normalized) {
211
+ if (['apikey', 'password', 'passwd', 'secret', 'credential', 'privatekey'].some((part) => normalized.includes(part))) return true;
212
+ if (normalized.includes('token')) return !NON_CREDENTIAL_TOKEN_KEYS.has(normalized);
213
+ if (normalized.includes('auth')) return !normalized.startsWith('author');
214
+ return false;
215
+ }
216
+
206
217
  function validatePortableSettingValue(value, label) {
207
218
  if (Array.isArray(value)) {
208
219
  value.forEach((item, index) => validatePortableSettingValue(item, `${label}[${index}]`));
209
220
  } else if (isRecord(value)) {
210
221
  for (const [key, nested] of Object.entries(value)) {
211
222
  const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, '');
212
- if (/(?:apikey|token|password|secret|credential|auth)/.test(normalized)) {
213
- throw new Error(`${label} contains a credential-like key (${key})`);
223
+ if (isCredentialLikeKey(normalized)) {
224
+ if (typeof nested !== 'string' || !ENV_REFERENCE.test(nested)) {
225
+ throw new Error(`${label} contains a credential-like key (${key}); only a $NAME or ${'$'}{NAME} environment reference may be managed`);
226
+ }
227
+ continue;
214
228
  }
215
229
  validatePortableSettingValue(nested, `${label}.${key}`);
216
230
  }
@@ -303,7 +317,7 @@ async function validateRepository(repo, { allowMissingPins = false } = {}) {
303
317
  const segments = decodePointer(field.path, `${fieldLabel}.path`);
304
318
  validatePortableSettingValue(field.value, `${fieldLabel}.value`);
305
319
  for (const segment of segments) {
306
- if (/(?:apikey|token|password|secret|credential|auth)/.test(segment.toLowerCase().replace(/[^a-z0-9]/g, ''))) {
320
+ if (isCredentialLikeKey(segment.toLowerCase().replace(/[^a-z0-9]/g, ''))) {
307
321
  throw new Error(`${fieldLabel}.path contains a credential-like key`);
308
322
  }
309
323
  }